chore: break down form-object by type, part 1

This commit is contained in:
waterplea
2023-03-22 17:05:24 +08:00
committed by Aiden McClelland
parent aa950669f6
commit ddf1f9bcd5
13 changed files with 287 additions and 186 deletions

View File

@@ -1,6 +1,6 @@
<ion-button
*ngIf="data.description"
class="slot-start"
class="icon"
fill="clear"
(click.stop)="presentAlertDescription()"
>

View File

@@ -1,9 +1,11 @@
:host {
display: flex;
align-items: center;
font-weight: bold;
}
.slot-start {
.icon {
--padding-start: 0;
--padding-end: 7px;
--padding-end: 4px;
margin-right: 4px;
}

View File

@@ -10,9 +10,9 @@ import { AlertController } from '@ionic/angular'
export class FormLabelComponent {
@Input() data!: {
name: string
new: boolean
edited: boolean
description: string | null
edited?: boolean
new?: boolean
required?: boolean
newOptions?: boolean
}
@@ -22,7 +22,7 @@ export class FormLabelComponent {
async presentAlertDescription() {
const alert = await this.alertCtrl.create({
header: this.data.name,
message: this.data.description!,
message: this.data.description || '',
buttons: [
{
text: 'OK',

View File

@@ -16,6 +16,9 @@ import {
ToEnumListDisplayPipe,
ToRangePipe,
} from './form-object.pipes'
import { FormFileComponent } from './form-object/controls/form-file/form-file.component'
import { FormInputComponent } from './form-object/controls/form-input/form-input.component'
import { FormWarningDirective } from './form-warning.directive'
@NgModule({
declarations: [
@@ -27,6 +30,9 @@ import {
ToEnumListDisplayPipe,
ToElementIdPipe,
ToRangePipe,
FormWarningDirective,
FormFileComponent,
FormInputComponent,
],
imports: [
CommonModule,

View File

@@ -0,0 +1,40 @@
<ion-item style="--padding-start: 0">
<form-label
[data]="{
name: spec.name,
description: spec.description,
edited: control.dirty
}"
></form-label>
<div slot="end">
<ion-button
*ngIf="!control.value; else hasFile"
strong
color="dark"
size="small"
(click)="uploadFile.click()"
>
Browse...
</ion-button>
<input
type="file"
[accept]="spec.extensions.join(',')"
style="display: none"
#uploadFile
(change)="handleFileInput($event)"
/>
<ng-template #hasFile>
<div class="inline">
<p class="ion-padding-end">{{ control.value.name }}</p>
<div style="cursor: pointer" (click)="clearFile()">
<ion-icon name="close"></ion-icon>
</div>
</div>
</ng-template>
</div>
</ion-item>
<p class="error-message">
<span *ngIf="control.errors as errors">
{{ errors | getError }}
</span>
</p>

View File

@@ -0,0 +1,26 @@
:host {
display: block;
}
ion-item-divider {
text-transform: unset;
border-bottom: 1px solid
var(
--ion-item-border-color,
var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13)))
);
--padding-top: 18px;
--padding-start: 0;
&.error-border {
border-color: var(--ion-color-danger-shade);
--border-color: var(--ion-color-danger-shade);
}
}
.error-message {
margin-top: 2px;
font-size: small;
color: var(--ion-color-danger);
}

View File

@@ -0,0 +1,21 @@
import { Component, Input } from '@angular/core'
import { AbstractControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
@Component({
selector: 'form-file',
templateUrl: './form-file.component.html',
styleUrls: ['./form-file.component.scss'],
})
export class FormFileComponent {
@Input() spec!: ValueSpecOf<'file'>
@Input() control!: AbstractControl
handleFileInput(e: any) {
this.control.patchValue(e.target.files[0])
}
clearFile() {
this.control.patchValue(null)
}
}

View File

@@ -0,0 +1,63 @@
<form-label
class="label"
[data]="{
name: spec.name,
description: spec.description,
new: form.original?.[name] === undefined,
edited: control.dirty,
required: !spec.nullable
}"
></form-label>
<ion-item [color]="(theme$ | async) === 'Light' ? 'light' : 'dark'">
<ion-textarea
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
formWarning
#warning="formWarning"
[placeholder]="spec.placeholder"
[formControl]="control"
(ionFocus)="warning.onChange(name, spec)"
(ionChange)="onInputChange.emit()"
></ion-textarea>
<ng-template #notTextArea>
<ion-input
formWarning
#warning="formWarning"
type="text"
class="input"
[class.input_redacted]="
spec.type === 'string' && control.value && spec.masked && !unmasked
"
[inputmode]="spec.type === 'number' ? 'tel' : 'text'"
[placeholder]="spec.placeholder"
[formControl]="control"
(ionFocus)="warning.onChange(name, spec)"
(ionChange)="onInputChange.emit()"
></ion-input>
</ng-template>
<ion-button
*ngIf="spec.type === 'string' && spec.masked"
slot="end"
fill="clear"
color="light"
(click)="unmasked = !unmasked"
>
<ion-icon
slot="icon-only"
size="small"
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
></ion-icon>
</ion-button>
<ion-note
*ngIf="spec.type === 'number' && spec.units"
slot="end"
color="light"
class="units"
>
{{ spec.units }}
</ion-note>
</ion-item>
<p class="error-message">
<span *ngIf="control.errors as errors">
{{ errors | getError : $any(spec)['pattern-description'] }}
</span>
</p>

View File

@@ -0,0 +1,24 @@
.label {
margin: 16px 0 6px;
}
.input {
font-family: 'Courier New';
font-weight: bold;
--placeholder-font-weight: 400;
&_redacted {
font-family: 'Redacted';
}
}
.units {
font-size: medium;
}
.error-message {
margin-top: 2px;
font-size: small;
color: var(--ion-color-danger);
}

View File

@@ -0,0 +1,24 @@
import { Component, Input, inject, Output, EventEmitter } from '@angular/core'
import { FormControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
import { THEME } from '@start9labs/shared'
import { FormObjectComponent } from '../../form-object.component'
@Component({
selector: 'form-input',
templateUrl: './form-input.component.html',
styleUrls: ['./form-input.component.scss'],
})
export class FormInputComponent {
@Input() name!: string
@Input() spec!: ValueSpecOf<'string' | 'number'>
@Input() control!: FormControl
@Output() onInputChange = new EventEmitter<void>()
unmasked = false
readonly theme$ = inject(THEME)
constructor(readonly form: FormObjectComponent) {}
}

View File

@@ -1,104 +1,33 @@
<ion-item-group [formGroup]="formGroup">
<div *ngFor="let entry of formGroup.controls | keyvalue : asIsOrder">
<div *ngIf="objectSpec[entry.key] as spec">
<!-- file -->
<form-file
*ngIf="spec.type === 'file'"
[spec]="spec"
[control]="entry.value"
></form-file>
<!-- string or number -->
<ng-container *ngIf="spec.type === 'string' || spec.type === 'number'">
<!-- label -->
<p class="input-label">
<form-label
[data]="{
name: spec.name,
description: spec.description,
new: original?.[entry.key] === undefined,
edited: entry.value.dirty,
required: !spec.nullable
}"
></form-label>
</p>
<ion-item [color]="(theme$ | async) === 'Light' ? 'light' : 'dark'">
<ion-textarea
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
[placeholder]="spec.placeholder"
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
></ion-textarea>
<ng-template #notTextArea>
<ion-input
type="text"
class="input"
[class.input_redacted]="
spec.type === 'string' &&
entry.value.value &&
spec.masked &&
!unmasked[entry.key]
"
[inputmode]="spec.type === 'number' ? 'tel' : 'text'"
[placeholder]="spec.placeholder"
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
></ion-input>
</ng-template>
<ion-button
*ngIf="spec.type === 'string' && spec.masked"
slot="end"
fill="clear"
color="light"
(click)="unmasked[entry.key] = !unmasked[entry.key]"
>
<ion-icon
slot="icon-only"
[name]="unmasked[entry.key] ? 'eye-off-outline' : 'eye-outline'"
size="small"
></ion-icon>
</ion-button>
<ion-note
*ngIf="spec.type === 'number' && spec.units"
slot="end"
color="light"
style="font-size: medium"
>
{{ spec.units }}
</ion-note>
</ion-item>
<p class="error-message">
<span *ngIf="formGroup.get(entry.key)?.errors as errors">
{{ errors | getError : $any(spec)['pattern-description'] }}
</span>
</p>
</ng-container>
<form-input
*ngIf="spec.type === 'string' || spec.type === 'number'"
[spec]="spec"
[name]="entry.key"
[control]="$any(entry.value)"
(onInputChange)="handleInputChange()"
></form-input>
<!-- boolean or enum -->
<ion-item
*ngIf="spec.type === 'boolean' || spec.type === 'enum'"
style="--padding-start: 0"
>
<ion-button
*ngIf="spec.description as desc"
fill="clear"
(click.stop)="presentAlertDescription(spec.name, desc)"
style="--padding-start: 0"
>
<ion-icon
name="help-circle-outline"
slot="icon-only"
size="small"
></ion-icon>
</ion-button>
<ion-label>
<b>
{{ spec.name }}
<ion-text
*ngIf="original?.[entry.key] === undefined"
color="success"
>
(New)
</ion-text>
<ion-text *ngIf="entry.value.dirty" color="warning">
(Edited)
</ion-text>
</b>
</ion-label>
<form-label
[data]="{
name: spec.name,
description: spec.description,
new: original?.[entry.key] === undefined,
edited: entry.value.dirty
}"
></form-label>
<!-- boolean -->
<ion-toggle
*ngIf="spec.type === 'boolean'"
@@ -109,7 +38,7 @@
<!-- enum -->
<!-- class enter-click disables the enter click on the modal behind the select -->
<ion-select
*ngIf="spec.type === 'enum' && formGroup.get(entry.key) as control"
*ngIf="spec.type === 'enum'"
[interfaceOptions]="{
message: spec.warning | toWarningText,
cssClass: 'enter-click'
@@ -117,7 +46,7 @@
slot="end"
placeholder="Select"
[formControlName]="entry.key"
[selectedText]="spec['value-names'][control.value]"
[selectedText]="spec['value-names'][entry.value.value]"
>
<ion-select-option
*ngFor="let option of spec.values"
@@ -127,64 +56,6 @@
</ion-select-option>
</ion-select>
</ion-item>
<!-- file -->
<ng-container
*ngIf="spec.type === 'file' && formGroup.get(entry.key) as control"
>
<ion-item style="--padding-start: 0">
<ion-button
*ngIf="spec.description as desc"
fill="clear"
(click.stop)="presentAlertDescription(spec.name, desc)"
style="--padding-start: 0"
>
<ion-icon
name="help-circle-outline"
slot="icon-only"
size="small"
></ion-icon>
</ion-button>
<ion-label>
<b>
{{ spec.name }}
<ion-text *ngIf="entry.value.dirty" color="warning">
(Edited)
</ion-text>
</b>
</ion-label>
<div slot="end">
<ion-button
*ngIf="!control.value; else hasFile"
strong
color="dark"
size="small"
(click)="uploadFile.click()"
>
Browse...
</ion-button>
<input
type="file"
[accept]="spec.extensions.join(',')"
style="display: none"
#uploadFile
(change)="handleFileInput($event, control)"
/>
<ng-template #hasFile>
<div class="inline">
<p class="ion-padding-end">{{ control.value.name }}</p>
<div style="cursor: pointer" (click)="clearFile(control)">
<ion-icon name="close"></ion-icon>
</div>
</div>
</ng-template>
</div>
</ion-item>
<p class="error-message">
<span *ngIf="control.errors as errors">
{{ errors | getError }}
</span>
</p>
</ng-container>
<!-- object -->
<ng-container *ngIf="spec.type === 'object'">
<!-- label -->

View File

@@ -7,12 +7,7 @@ import {
inject,
SimpleChanges,
} from '@angular/core'
import {
AbstractControl,
FormArray,
UntypedFormArray,
UntypedFormGroup,
} from '@angular/forms'
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
import {
InputSpec,
@@ -240,28 +235,6 @@ export class FormObjectComponent {
await alert.present()
}
handleFileInput(e: any, control: AbstractControl) {
control.patchValue(e.target.files[0])
}
clearFile(control: AbstractControl) {
control.patchValue(null)
}
async presentAlertDescription(name: string, description: string) {
const alert = await this.alertCtrl.create({
header: name,
message: description || '',
buttons: [
{
text: 'OK',
cssClass: 'enter-click',
},
],
})
await alert.present()
}
private addListItem(key: string): void {
const arr = this.formGroup.get(key) as UntypedFormArray
const listSpec = this.objectSpec[key] as ValueSpecList

View File

@@ -0,0 +1,51 @@
import { Directive } from '@angular/core'
import { ValueSpec, ValueSpecUnion } from 'start-sdk/types/config-types'
import { AlertButton, AlertController } from '@ionic/angular'
@Directive({
selector: '[formWarning]',
exportAs: 'formWarning',
})
export class FormWarningDirective {
private warned = false
constructor(private readonly alertCtrl: AlertController) {}
async onChange<T extends ValueSpec>(
key: string,
spec: T extends ValueSpecUnion ? never : T,
okFn?: Function,
cancelFn?: Function,
) {
if (!spec.warning || this.warned) return okFn ? okFn() : null
this.warned = true
const buttons: AlertButton[] = [
{
text: 'Ok',
handler: () => {
if (okFn) okFn()
},
cssClass: 'enter-click',
},
]
if (okFn || cancelFn) {
buttons.unshift({
text: 'Cancel',
handler: () => {
if (cancelFn) cancelFn()
},
})
}
const alert = await this.alertCtrl.create({
header: 'Warning',
subHeader: `Editing ${spec.name} has consequences:`,
message: spec.warning,
buttons,
})
await alert.present()
}
}