Skip to content

Commit

Permalink
Separate out control concerns from renderer concerns
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanFarmer committed Mar 7, 2024
1 parent 6e6ea42 commit ba8c728
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 146 deletions.
86 changes: 24 additions & 62 deletions src/controls/NumericControl/NumericControl.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,50 @@
import { ControlProps, RendererProps } from "@jsonforms/core"
import { Col, Form, InputNumber } from "antd"
import { coerceToInteger, coerceToNumber, decimalToPercentage } from "../utils"
import { Col, Form } from "antd"
import { NumericInput } from "../../renderers/NumericInput"
import { coerceToInteger, coerceToNumber } from "../utils"


export const NumericControl = ({
data,
handleChange,
path,
required,
label,
visible,
id,
schema,
uischema,
}: ControlProps & RendererProps) => {
if (!visible) return null
export const NumericControl = (props: ControlProps & RendererProps) => {
if (!props.visible) return null

const arialLabelWithFallback = label || schema.description || "Value"
const schema = props.schema

const minimum = schema.properties?.minimum as number
const maximum = schema.properties?.maximum as number

const defaultValue = typeof schema?.default === "number" ? schema.default : undefined
const isEmptyObj = typeof data === "object" && data !== undefined && data !== null ? Object.keys(data as object).length === 0 : false
const value = data === undefined || isEmptyObj ? defaultValue : 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 ariaLabel = props.label || schema.description || "Value"
const initialValue = typeof schema.default === "number" ? schema.default : undefined

const numberType = schema.properties?.type as string
const onChange = (value: number | null) => {
if (value !== null) {
if (numberType === "integer") {
handleChange(path, value !== null ? coerceToInteger(value) : value)
props.handleChange(props.path, value !== null ? coerceToInteger(value) : value)
} else {
handleChange(path, value !== null ? coerceToNumber(value) : value)
props.handleChange(props.path, value !== null ? coerceToNumber(value) : value)
}
}
}

const style = { marginLeft: 0, 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}
defaultValue={defaultValue}
value={value}
onChange={onChange}
min={minimum}
max={maximum}
addonBefore={addonBefore}
addonAfter={addonAfter}
formatter={formatter}
style={style}
controls={false}
/>
)

const rules = [
{ required, message: required ? `${label} is required` : "" },
{ message: props.required ? `${props.label} is required` : "" },
]

const numericInputProps = {
...props,
...onChange,
"aria-label": ariaLabel,
}
const numericInput = NumericInput(numericInputProps)

return (
<Form.Item
label={label}
id={id}
name={path}
required={required}
initialValue={defaultValue}
label={props.label}
id={props.id}
name={props.path}
required={props.required}
initialValue={initialValue}
rules={rules}
validateTrigger={["onBlur"]}
>
<Col span={18}>{numberInput}</Col>
<Col span={18}>{numericInput}</Col>
</Form.Item>
)
}
Expand Down
117 changes: 33 additions & 84 deletions src/controls/NumericControl/NumericSliderControl.tsx
Original file line number Diff line number Diff line change
@@ -1,108 +1,57 @@
import { ControlProps, RendererProps } from "@jsonforms/core"
import { Col, Form, InputNumber, Slider } from "antd"
import { coerceToNumber, coerceToInteger, decimalToPercentage } from "../utils"
import { Col, Form } from "antd"
import { NumericInput } from "../../renderers/NumericInput"
import { NumericSlider } from "../../renderers/NumericSlider"
import { coerceToInteger, coerceToNumber } from "../utils"


export function NumericSliderControl({
data,
handleChange,
path,
required,
label,
visible,
id,
schema,
uischema,
}: ControlProps & RendererProps) {
if (!visible) return null
export const NumericSliderControl = (props: ControlProps & RendererProps) => {
if (!props.visible) return null

const arialLabelWithFallback = label || schema.description || "Value"
const schema = props.schema

const minimum = schema.properties?.minimum as number
const maximum = schema.properties?.maximum as number
const step = schema.properties?.multipleOf as number
const initialValue = typeof schema.default === "number" ? schema.default : undefined

const defaultValue = 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 ? defaultValue : 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 numberType = schema.properties?.type as string
const numberType = props.schema.properties?.type as string
const minimum = props.schema.properties?.minimum as number | undefined
const maximum = props.schema.properties?.maximum as number | undefined
const onChange = (value: number | null) => {
if (value !== null && value >= minimum && value <= maximum) {
if (value !== null && (minimum && value >= minimum) && (maximum && value <= maximum)) {
if (numberType === "integer") {
handleChange(path, value !== null ? coerceToInteger(value) : value)
} else {
handleChange(path, value !== null ? coerceToNumber(value) : value)
}
}
}

const style = { marginLeft: 16, width: "100%" }
const formatter = ((value?: number) => {
if (typeof value !== "undefined") {
if (isPercentage) {
return decimalToPercentage(value)
props.handleChange(props.path, value !== null ? coerceToInteger(value) : value)
} else {
return value.toString()
props.handleChange(props.path, value !== null ? coerceToNumber(value) : value)
}
}
return ""
})

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

const tooltip = {
formatter: (value?: number) => {
const tooltipValue = value !== undefined ? value : defaultValue
const formattedTooltipValue = isPercentage ? decimalToPercentage(tooltipValue) : tooltipValue
return `${addonBefore ? addonBefore : ""}${formattedTooltipValue}${addonAfter ? addonAfter : ""}`
}
}

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

const rules = [
{ required, message: required ? `${label} is required` : "" },
{ message: props.required ? `${props.label} is required` : "" },
]

const numericSliderProps = {
...props,
...onChange,
}
const numericSlider = NumericSlider(numericSliderProps)

const numericInputProps = {
...numericSliderProps,
"aria-label": props.label || schema.description || "Value",
}
const numericInput = NumericInput(numericInputProps)

return (
<Form.Item
label={label}
id={id}
name={path}
required={required}
initialValue={defaultValue}
label={props.label}
id={props.id}
name={props.path}
required={props.required}
initialValue={initialValue}
rules={rules}
validateTrigger={["onBlur"]}
>
<Col span={8}>{slider}</Col><Col span={7}>{numberInput}</Col>
<Col span={8}>{numericSlider}</Col><Col span={7}>{numericInput}</Col>
</Form.Item>
)
}
Empty file removed src/controls/testerConditions.ts
Empty file.
50 changes: 50 additions & 0 deletions src/renderers/NumericInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ReactElement } from "react"
import { RendererProps } from "@jsonforms/core"
import { InputNumber } from "antd"
import { decimalToPercentage } from "../controls/utils"

type NumericInput = ReactElement<typeof InputNumber>
type NumericInputProps = RendererProps & React.ComponentProps<typeof InputNumber>

export const NumericInput = (props: NumericInputProps): NumericInput => {
const schema = props.schema

const min = schema.properties?.minimum as number | undefined
const max = schema.properties?.maximum as number | undefined

const defaultValue = typeof schema?.default === "number" ? schema.default : undefined

const isNonNullObject = typeof props.data === "object" && props.data !== undefined && props.data !== null
const isEmptyObj = isNonNullObject ? Object.keys(props.data as object).length === 0 : false
const value = props.data === undefined || isEmptyObj ? defaultValue : props.data as number | null

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

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

return <InputNumber
aria-label={props["aria-label"]}
defaultValue={defaultValue}
value={value}
onChange={props.onChange}
min={min}
max={max}
addonBefore={addonBefore}
addonAfter={addonAfter}
formatter={formatter}
style={style}
controls={false}
/>
}
45 changes: 45 additions & 0 deletions src/renderers/NumericSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ReactElement } from "react"
import { RendererProps } from "@jsonforms/core"
import { Slider } from "antd"
import { decimalToPercentage } from "../controls/utils"

type NumericSlider = ReactElement<typeof Slider>
type NumericSliderProps = RendererProps & React.ComponentProps<typeof Slider>

export const NumericSlider = (props: NumericSliderProps): NumericSlider => {
const schema = props.schema

const min = schema.properties?.minimum as number | undefined
const max = schema.properties?.maximum as number | undefined
const step = schema.properties?.multipleOf as number | undefined

const defaultValue = typeof schema?.default === "number" ? schema.default : min

const isNonNullObject = typeof props.data === "object" && props.data !== undefined && props.data !== null
const isEmptyObj = isNonNullObject ? Object.keys(props.data as object).length === 0 : false
const value = props.data === undefined || isEmptyObj ? defaultValue : props.data as number | null

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

const tooltip = {
formatter: (value?: number) => {
const tooltipValue = value !== undefined ? value : defaultValue
const formattedTooltipValue = isPercentage ? decimalToPercentage(tooltipValue) : tooltipValue
return `${addonBefore ? addonBefore : ""}${formattedTooltipValue}${addonAfter ? addonAfter : ""}`
}
}

return <Slider
defaultValue={defaultValue}
value={value === null ? defaultValue : value}
disabled={defaultValue === null}
onChange={props.onChange as (value: number) => void}
min={min}
max={max}
step={step}
tooltip={tooltip}
range={false}
/>
}

0 comments on commit ba8c728

Please sign in to comment.