Skip to content

Commit

Permalink
feat: Add NumberControl (#16)
Browse files Browse the repository at this point in the history
* Port over existing code

* Fix typing errors

* Exclude .ts files from prodution build

* Exclude all files with .test. from prod build

* Add untracked file

* Remove unused file

* Move tests to another branch

* Push trivial change

* Revert omit change

* Remove NumericControlProps and use cast

* Add existing tests

* Revert "Add existing tests"

This reverts commit 3c671dd.

* Update to new registry pattern

* Pass min, max, step, and controls to InputNumber also

* Only show percentage addonAfter if isPercentage

* Only show percentage addonAfter if isPercentage

* Fix marginLeft on pure number slider

* Update tooltip logic for non-percentages on slider

* Update logic to fill functionality gaps

* Add logic for required asterisk

* Fix tooltips

* Update value default logic

* Fix more broken empty value logic
  • Loading branch information
NathanFarmer committed Mar 5, 2024
1 parent 48d8872 commit 1186933
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 13 deletions.
8 changes: 0 additions & 8 deletions src/controls/LabelRendererRegistryEntry.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/controls/NumberControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createNumericControl } from "./NumericControl"

export const NumberControl = createNumericControl({
coerceNumber: (value) => Number(value),
})
122 changes: 122 additions & 0 deletions src/controls/NumericControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ControlProps, RendererProps } from "@jsonforms/core"
import { Col, Form, InputNumber, Row, Slider } from "antd"
import { decimalToPercentage } from "./utils"



export const createNumericControl = (args: { coerceNumber: (value: number) => number; pattern?: string }) => {
return function NumericControl({
data,
handleChange,
path,
required,
label,
visible,
id,
schema,
uischema,
}: ControlProps & RendererProps) {
const arialLabelWithFallback = label || schema.description || "Value"
const isRequired = required || uischema.options?.required as boolean

const maxStepsWithoutTextInput = 100
const { maximum, minimum, multipleOf } = schema
const isRangeDefined = typeof maximum === "number" && typeof minimum === "number"
let step: number | undefined = undefined
let stepCount: number | undefined = undefined
if (isRangeDefined) {
const range = Math.abs(maximum - minimum)
step = multipleOf || (range / maxStepsWithoutTextInput)
stepCount = range / step
}
const isLargeStepCount = stepCount && stepCount > maxStepsWithoutTextInput

const initialValue: number | undefined = typeof schema?.default === "number" ? schema.default : minimum
const isEmptyObj = typeof data === "object" && data !== undefined && data !== null ? Object.keys(data as object).length === 0 : false
const value = data === undefined || isEmptyObj ? initialValue : data as number | null

const addonAfter = uischema.options?.addonAfter as string | undefined
const addonBefore = uischema.options?.addonBefore as string | undefined
const isPercentage = addonAfter?.trim() === "%"

const onChange = (value: number | null) => {
if ((typeof value === "number" && (!isRangeDefined || (isRangeDefined && value >= minimum && value <= maximum))) || value === null) {
handleChange(path, value !== null ? args.coerceNumber(value) : value)
}
}

const marginLeft = isRangeDefined ? 16 : 0
const style = { marginLeft: marginLeft, width: "100%" }
const formatter = ((value?: number) => {
if (typeof value !== "undefined") {
if (isPercentage) {
return decimalToPercentage(value)
} else {
return value.toString()
}
}
return ""
})

const numberInput = (
<InputNumber
aria-label={arialLabelWithFallback}
value={value}
defaultValue={initialValue}
pattern={args.pattern}
onChange={onChange}
style={style}
max={maximum}
min={minimum}
formatter={formatter}
controls={false}
addonAfter={addonAfter}
addonBefore={addonBefore}
/>
)

if (!visible) return null

const tooltip = {
formatter: (value?: number) => {
if (isPercentage) {
return `${decimalToPercentage(value || initialValue)}%`
} else {
return `${addonBefore ? addonBefore : ""}${value || initialValue}${addonAfter ? addonAfter : ""}`
}
}
}

const slider = <Slider
value={value === null ? initialValue : value}
defaultValue={initialValue}
min={minimum}
max={maximum}
disabled={initialValue === null}
onChange={onChange}
step={step}
tooltip={tooltip}
/>

return (
<Form.Item
label={label}
id={id}
name={path}
required={isRequired}
initialValue={initialValue}
rules={[{ required, message: required ? `${label} is required` : "" }]}
validateTrigger={["onBlur"]}
>
{isRangeDefined ? (
<Row>
<Col span={8}>{slider}</Col>
{isLargeStepCount ? <Col span={7}>{numberInput}</Col> : null}
</Row>
) : (
<Col span={18}>{numberInput}</Col>
)}
</Form.Item>
)
}
}
9 changes: 9 additions & 0 deletions src/controls/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function decimalToPercentage(value?: number) {
if (value === undefined) return ""
const percentage = parseFloat((value * 100).toFixed(10)) // accounting for 10 digits after the decimal point
return `${percentage}`
}

export function percentageStringToDecimal(value: string | undefined) {
return Number(value) / 100
}
12 changes: 7 additions & 5 deletions src/renderers.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { JsonFormsRendererRegistryEntry,JsonFormsCellRendererRegistryEntry, isBooleanControl, isStringControl, rankWith, uiTypeIs } from "@jsonforms/core";
import { JsonFormsRendererRegistryEntry,JsonFormsCellRendererRegistryEntry, isBooleanControl, isNumberControl, isStringControl, rankWith, uiTypeIs } from "@jsonforms/core";
import { withJsonFormsControlProps, withJsonFormsLabelProps, withJsonFormsCellProps, withJsonFormsLayoutProps } from "@jsonforms/react";

import { BooleanControl } from "./controls/BooleanControl";
import { AlertControl } from "./controls/AlertControl";
import { TextControl } from "./controls/TextControl";
import { UnknownControl } from "./controls/UnknownControl";
import { VerticalLayoutRenderer } from "./layouts/VerticalLayout";
import { NumberControl } from "./controls/NumberControl";


// Ordered from lowest rank to highest rank. Higher rank renderers will be preferred over lower rank renderers.
export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [
{ tester: rankWith(1, () => true), renderer: withJsonFormsControlProps(UnknownControl)},
{ tester: rankWith(2, uiTypeIs("VerticalLayout")), renderer: withJsonFormsLayoutProps(VerticalLayoutRenderer)},
{ tester: rankWith(1, () => true), renderer: withJsonFormsControlProps(UnknownControl) },
{ tester: rankWith(2, uiTypeIs("VerticalLayout")), renderer: withJsonFormsLayoutProps(VerticalLayoutRenderer) },
{ tester: rankWith(2, isBooleanControl), renderer: withJsonFormsControlProps(BooleanControl) },
{ tester: rankWith(2, isStringControl), renderer: withJsonFormsControlProps(TextControl)},
{ tester: rankWith(100, uiTypeIs("Label")),renderer: withJsonFormsLabelProps(AlertControl)},
{ tester: rankWith(2, isStringControl), renderer: withJsonFormsControlProps(TextControl) },
{ tester: rankWith(2, uiTypeIs("Label")), renderer: withJsonFormsLabelProps(AlertControl) },
{ tester: rankWith(2, isNumberControl), renderer: withJsonFormsControlProps(NumberControl) },
];

export const cellRegistryEntries: JsonFormsCellRendererRegistryEntry[] = [
Expand Down

0 comments on commit 1186933

Please sign in to comment.