feat: implement disabled, immutable and generate (#2280)

* feat: implement `disabled`, `immutable` and `generate`

* chore: remove unnecessary code

* chore: add generate to textarea and implement immutable

* no generate for textarea

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Alex Inkin
2023-05-22 23:40:06 +04:00
committed by Aiden McClelland
parent 4d1c7a3884
commit 8aa19e6420
26 changed files with 215 additions and 85 deletions

View File

@@ -6,6 +6,7 @@ import { TUI_DATE_FORMAT, TUI_DATE_SEPARATOR } from '@taiga-ui/cdk'
import {
tuiButtonOptionsProvider,
tuiNumberFormatProvider,
tuiTextfieldOptionsProvider,
} from '@taiga-ui/core'
import {
TUI_DATE_TIME_VALUE_TRANSFORMER,
@@ -33,6 +34,7 @@ export const APP_PROVIDERS: Provider[] = [
IonNav,
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
tuiButtonOptionsProvider({ size: 'm' }),
tuiTextfieldOptionsProvider({ hintOnDisabled: true }),
{
provide: TUI_DATE_FORMAT,
useValue: 'MDY',

View File

@@ -14,6 +14,13 @@ export abstract class Control<Spec extends ValueSpec, Value> {
return this.control.spec
}
// TODO: Properly handle already set immutable value
get readOnly(): boolean {
return (
!!this.value && !!this.control.control?.pristine && this.control.immutable
)
}
get value(): Value | null {
return this.control.value
}

View File

@@ -1,8 +1,8 @@
<div class="label">
{{ spec.name }}
<tui-tooltip
*ngIf="spec.description"
[content]="spec.description"
*ngIf="spec.description || spec.disabled"
[content]="spec | hint"
></tui-tooltip>
<button
tuiLink

View File

@@ -37,8 +37,9 @@ export class FormArrayComponent {
get canAdd(): boolean {
return (
!this.spec.maxLength ||
this.spec.maxLength >= this.array.control.controls.length
!this.spec.disabled &&
(!this.spec.maxLength ||
this.spec.maxLength >= this.array.control.controls.length)
)
}

View File

@@ -2,7 +2,9 @@
[maskito]="mask"
[tuiTextfieldCustomContent]="color"
[tuiTextfieldCleaner]="false"
[tuiHintContent]="spec.description"
[tuiHintContent]="spec | hint"
[readOnly]="readOnly"
[disabled]="!!spec.disabled"
[pseudoInvalid]="invalid"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
@@ -13,6 +15,7 @@
<ng-template #color>
<div class="wrapper" [style.color]="value">
<input
*ngIf="!readOnly && !spec.disabled"
type="color"
class="color"
tabindex="-1"

View File

@@ -10,8 +10,13 @@
<form-toggle *ngSwitchCase="'toggle'"></form-toggle>
</ng-container>
<tui-error [error]="order | tuiFieldError | async"></tui-error>
<ng-template *ngIf="spec.warning" #warning let-completeWith="completeWith">
<ng-template
*ngIf="spec.warning || immutable"
#warning
let-completeWith="completeWith"
>
{{ spec.warning }}
<p *ngIf="immutable">This value cannot be changed once set!</p>
<div class="buttons">
<button
tuiButton

View File

@@ -39,6 +39,10 @@ export class FormControlComponent<
readonly order = ERRORS
private readonly alerts = inject(TuiAlertService)
get immutable(): boolean {
return 'immutable' in this.spec && this.spec.immutable
}
onFocus(focused: boolean) {
this.focused = focused
this.updateFocused(focused)

View File

@@ -1,6 +1,9 @@
<ng-container [ngSwitch]="spec.inputmode" [tuiHintContent]="spec.description">
<tui-input-time
*ngSwitchCase="'time'"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[ngModel]="getTime(value)"
(ngModelChange)="value = $event?.toString() || null"
@@ -11,6 +14,9 @@
</tui-input-time>
<tui-input-date
*ngSwitchCase="'date'"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max"
@@ -22,6 +28,9 @@
</tui-input-date>
<tui-input-date-time
*ngSwitchCase="'datetime-local'"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper : getLimit) : min"
[max]="spec.max ? (spec.max | tuiMapper : getLimit) : max"

View File

@@ -1,5 +1,7 @@
<tui-multi-select
[tuiHintContent]="spec.description"
[tuiHintContent]="spec | hint"
[disabled]="disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[editable]="false"
[disabledItemHandler]="disabledItemHandler"

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
import { ValueSpecMultiselect } from '@start9labs/start-sdk/lib/config/configTypes'
import { Control } from '../control'
import { tuiPure } from '@taiga-ui/cdk'
import { invert } from '@start9labs/shared'
@Component({
selector: 'form-multiselect',
@@ -11,13 +12,26 @@ export class FormMultiselectComponent extends Control<
ValueSpecMultiselect,
readonly string[]
> {
readonly items = Object.values(this.spec.values)
private readonly inverted = invert(this.spec.values)
readonly disabledItemHandler = (item: string): boolean =>
private readonly isDisabled = (item: string) =>
Array.isArray(this.spec.disabled) &&
this.spec.disabled.includes(this.inverted[item])
private readonly isExceedingLimit = (item: string) =>
!!this.spec.maxLength &&
this.selected.length >= this.spec.maxLength &&
!this.selected.includes(item)
readonly disabledItemHandler = (item: string): boolean =>
this.isDisabled(item) || this.isExceedingLimit(item)
readonly items = Object.values(this.spec.values)
get disabled(): boolean {
return typeof this.spec.disabled === 'string'
}
get selected(): string[] {
return this.memoize(this.value)
}

View File

@@ -1,5 +1,7 @@
<tui-input-number
[tuiHintContent]="spec.description"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[tuiTextfieldPostfix]="spec.units || ''"
[pseudoInvalid]="invalid"
[precision]="Infinity"

View File

@@ -1,5 +1,7 @@
<tui-select
[tuiHintContent]="spec.description"
[tuiHintContent]="spec | hint"
[disabled]="disabled"
[readOnly]="readOnly"
[tuiTextfieldCleaner]="!spec.required"
[pseudoInvalid]="invalid"
[(ngModel)]="selected"
@@ -9,7 +11,7 @@
<span *ngIf="spec.required">*</span>
<select
tuiSelect
[labels]="[spec.warning ? ' ' + spec.warning : '']"
[items]="[items]"
[items]="items"
[disabledItemHandler]="disabledItemHandler"
></select>
</tui-select>

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core'
import { ValueSpecSelect } from '@start9labs/start-sdk/lib/config/configTypes'
import { invert } from '@start9labs/shared'
import { Control } from '../control'
@Component({
@@ -7,15 +8,23 @@ import { Control } from '../control'
templateUrl: './form-select.component.html',
})
export class FormSelectComponent extends Control<ValueSpecSelect, string> {
private readonly inverted = invert(this.spec.values)
readonly items = Object.values(this.spec.values)
readonly disabledItemHandler = (item: string) =>
Array.isArray(this.spec.disabled) &&
this.spec.disabled.includes(this.inverted[item])
get disabled(): boolean {
return typeof this.spec.disabled === 'string'
}
get selected(): string | null {
return this.value && this.spec.values[this.value]
}
set selected(value: string | null) {
this.value =
Object.entries(this.spec.values).find(([_, v]) => value === v)?.[0] ??
null
this.value = (value && this.inverted[value]) || null
}
}

View File

@@ -1,6 +1,8 @@
<tui-input
[tuiTextfieldCustomContent]="spec.masked ? toggle : ''"
[tuiHintContent]="spec.description"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
@@ -17,13 +19,24 @@
/>
</tui-input>
<ng-template #toggle>
<button
*ngIf="spec.generate"
tuiIconButton
type="button"
appearance="icon"
title="Generate"
size="xs"
class="button"
icon="tuiIconRefreshCcw"
(click)="generate()"
></button>
<button
tuiIconButton
type="button"
appearance="icon"
title="Toggle masking"
size="xs"
class="toggle"
class="button"
[icon]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
(click)="masked = !masked"
></button>

View File

@@ -1,6 +1,6 @@
.toggle {
.button {
pointer-events: auto;
margin-left: auto;
margin-left: 0.25rem;
}
.masked {

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core'
import { ValueSpecText } from '@start9labs/start-sdk/lib/config/configTypes'
import { Control } from '../control'
import { getDefaultString } from '../../../util/config-utilities'
@Component({
selector: 'form-text',
@@ -9,4 +10,8 @@ import { Control } from '../control'
})
export class FormTextComponent extends Control<ValueSpecText, string> {
masked = true
generate() {
this.value = getDefaultString(this.spec.generate || '')
}
}

View File

@@ -1,5 +1,7 @@
<tui-text-area
[tuiHintContent]="spec.description"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[expandable]="true"
[rows]="6"

View File

@@ -1,10 +1,11 @@
{{ spec.name }}
<tui-tooltip
*ngIf="spec.description"
[content]="spec.description"
*ngIf="spec.description || spec.disabled"
[tuiHintContent]="spec | hint"
></tui-tooltip>
<tui-toggle
size="l"
[disabled]="!!spec.disabled || readOnly"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
></tui-toggle>

View File

@@ -48,6 +48,7 @@ import { MustachePipe } from './mustache.pipe'
import { ControlDirective } from './control.directive'
import { FormColorComponent } from './form-color/form-color.component'
import { FormDatetimeComponent } from './form-datetime/form-datetime.component'
import { HintPipe } from './hint.pipe'
@NgModule({
imports: [
@@ -98,6 +99,7 @@ import { FormDatetimeComponent } from './form-datetime/form-datetime.component'
FormObjectComponent,
FormArrayComponent,
MustachePipe,
HintPipe,
ControlDirective,
],
exports: [FormGroupComponent],

View File

@@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core'
import { ValueSpec } from '@start9labs/start-sdk/lib/config/configTypes'
@Pipe({
name: 'hint',
})
export class HintPipe implements PipeTransform {
transform(spec: ValueSpec): string {
const hint = []
if (spec.description) {
hint.push(spec.description)
}
if ('disabled' in spec && typeof spec.disabled === 'string') {
hint.push(`Disabled: ${spec.disabled}`)
}
return hint.join('\n\n')
}
}

View File

@@ -717,6 +717,18 @@ export module Mock {
),
}),
),
users: Value.multiselect({
name: 'Users',
default: [],
maxLength: 2,
disabled: ['matt'],
values: {
matt: 'Matt Hill',
alex: 'Alex Inkin',
blue: 'Blue J',
lucy: 'Lucy',
},
}),
advanced: Value.object(
{
name: 'Advanced',
@@ -900,19 +912,19 @@ export module Mock {
},
),
),
'random-enum': Value.select({
name: 'Random Enum',
'random-select': Value.select({
name: 'Random select',
description: 'This is not even real.',
warning: 'Be careful changing this!',
required: {
default: 'null',
default: null,
},
values: {
null: 'null',
option1: 'option1',
option2: 'option2',
option3: 'option3',
},
disabled: ['option2'],
}),
'favorite-number':
/* TODO: Convert range for this value ((-100,100])*/ Value.number({
@@ -1037,8 +1049,13 @@ export module Mock {
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
warning: 'Careful changing this',
required: { default: 'internal' },
disabled: ['fake'],
},
Variants.of({
fake: {
name: 'Fake',
spec: Config.of({}),
},
internal: {
name: 'Internal',
spec: Config.of({}),
@@ -1113,6 +1130,10 @@ export module Mock {
}),
'favorite-slogan': Value.text({
name: 'Favorite Slogan',
generate: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
required: false,
description:
'You most favorite slogan in the whole world, used for paying you.',

View File

@@ -52,8 +52,6 @@ export class FormService {
): ValueSpecSelect {
return {
...spec,
// TODO: implement disabled
disabled: false,
type: 'select',
default: selection,
values: Object.fromEntries(