Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
dieseldjango committed Mar 7, 2022
2 parents d2017b2 + afb4053 commit 523c360
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 19 deletions.
24 changes: 12 additions & 12 deletions packages/core/src/components/fields/ObjectField.js
Expand Up @@ -7,7 +7,8 @@ import {
retrieveSchema,
getDefaultRegistry,
canExpand,
ADDITIONAL_PROPERTY_FLAG,
isAddedProperty,
uiSchemaForProperty,
} from "../../utils";

function DefaultObjectFieldTemplate(props) {
Expand Down Expand Up @@ -72,12 +73,12 @@ class ObjectField extends Component {
!!v);
}

onPropertyChange = (name, addedByAdditionalProperties = false) => {
onPropertyChange = (name, addedProperty = false) => {
return (value, errorSchema) => {
if (value === undefined && addedByAdditionalProperties) {
if (value === undefined && addedProperty) {
// Don't set value = undefined for fields added by
// additionalProperties. Doing so removes them from the
// formData, which causes them to completely disappear
// additionalProperties or patternProperties. Doing so removes
// them from the formData, which causes them to completely disappear
// (including the input field for the property name). Unlike
// fields which are "mandated" by the schema, these fields can
// be set to undefined by clicking a "delete field" button, so
Expand Down Expand Up @@ -164,6 +165,9 @@ class ObjectField extends Component {
}

handleAddClick = schema => () => {
// TODO - problem: we won't know which patternProperty or additionalProperties schema to apply until we have a key
// solution? => if additionalProperties, use that type by default. On key change, update type to first matching
// patternProperty type, if available
let type = schema.additionalProperties.type;
const newFormData = { ...this.props.formData };

Expand Down Expand Up @@ -235,12 +239,8 @@ class ObjectField extends Component {
TitleField,
DescriptionField,
properties: orderedProperties.map(name => {
const addedByAdditionalProperties = schema.properties[
name
].hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
const fieldUiSchema = addedByAdditionalProperties
? uiSchema.additionalProperties
: uiSchema[name];
const addedProperty = isAddedProperty(schema.properties[name]);
const fieldUiSchema = uiSchemaForProperty(name, schema.properties, uiSchema);
const hidden = fieldUiSchema && fieldUiSchema["ui:widget"] === "hidden";

return {
Expand All @@ -260,7 +260,7 @@ class ObjectField extends Component {
onKeyChange={this.onKeyChange(name)}
onChange={this.onPropertyChange(
name,
addedByAdditionalProperties
addedProperty
)}
onBlur={onBlur}
onFocus={onFocus}
Expand Down
13 changes: 6 additions & 7 deletions packages/core/src/components/fields/SchemaField.js
Expand Up @@ -4,15 +4,14 @@ import PropTypes from "prop-types";
import * as types from "../../types";

import {
ADDITIONAL_PROPERTY_FLAG,
isSelect,
retrieveSchema,
toIdSchema,
getDefaultRegistry,
mergeObjects,
deepEquals,
getSchemaType,
getDisplayLabel,
getDisplayLabel, isAddedProperty,
} from "../../utils";

const REQUIRED_FIELD_SYMBOL = "*";
Expand Down Expand Up @@ -142,13 +141,13 @@ function DefaultTemplate(props) {
}

return (
<WrapIfAdditional {...props}>
<WrapIfAdded {...props}>
{displayLabel && <Label label={label} required={required} id={id} />}
{displayLabel && description ? description : null}
{children}
{errors}
{help}
</WrapIfAdditional>
</WrapIfAdded>
);
}
if (process.env.NODE_ENV !== "production") {
Expand Down Expand Up @@ -179,7 +178,7 @@ DefaultTemplate.defaultProps = {
displayLabel: true,
};

function WrapIfAdditional(props) {
function WrapIfAdded(props) {
const {
id,
classNames,
Expand All @@ -192,9 +191,9 @@ function WrapIfAdditional(props) {
schema,
} = props;
const keyLabel = `${label} Key`; // i18n ?
const additional = schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
const addedProperty = isAddedProperty(schema);

if (!additional) {
if (!addedProperty) {
return <div className={classNames}>{props.children}</div>;
}

Expand Down
87 changes: 87 additions & 0 deletions packages/core/src/utils.js
Expand Up @@ -2,13 +2,15 @@ import React from "react";
import * as ReactIs from "react-is";
import mergeAllOf from "json-schema-merge-allof";
import fill from "core-js-pure/features/array/fill";
import cloneDeep from "lodash/cloneDeep";
import union from "lodash/union";
import jsonpointer from "jsonpointer";
import fields from "./components/fields";
import widgets from "./components/widgets";
import validateFormData, { isValid } from "./validate";

export const ADDITIONAL_PROPERTY_FLAG = "__additional_property";
export const PATTERN_PROPERTY = "__pattern_property";

const widgetMap = {
boolean: {
Expand Down Expand Up @@ -676,6 +678,64 @@ export function stubExistingAdditionalProperties(
return schema;
}

// This function will create new "properties" items for each unassigned key matching the pattern(s) in our formData
export function stubExistingPatternProperties(
schema,
rootSchema = {},
formData = {}
) {
// Clone the schema so we don't ruin the consumer's original
schema = cloneDeep(schema);

if (!schema.properties) {
schema.properties = { };
}

const hasAdditionalProperties =
schema.hasOwnProperty("additionalProperties") &&
schema.additionalProperties !== false;

// make sure formData is an object
formData = isObject(formData) ? formData : {};

// TODO - dieseldjango: catch exception for invalid regex?
const patterns = Object.keys(schema.patternProperties).map(pattern => [pattern, new RegExp(pattern)]);
Object.keys(formData).forEach(key => {
let patternSchema;

if (schema.properties.hasOwnProperty(key)) {
// No need to stub, our schema already has the property
return;
}

let matchingPattern = patterns.find(([, regex]) => regex.test(key));
if (!matchingPattern && !hasAdditionalProperties) {
matchingPattern = patterns[0]; // default to first pattern if no match and no additional properties
}

if (matchingPattern) {
patternSchema = schema.patternProperties[matchingPattern[0]];

if (schema.hasOwnProperty("$ref")) {
patternSchema = retrieveSchema(
{ $ref: patternSchema["$ref"] },
rootSchema,
formData
);
}

// The type of our new key should match the patternProperties value;
schema.properties[key] = patternSchema;

// Set our pattern property so we know it was dynamically added
schema.properties[key][PATTERN_PROPERTY] = matchingPattern[0];
}
// remaining properties will be filled in as additionalProperties
});

return schema;
}

/**
* Resolves a conditional block (if/else/then) by removing the condition and merging the appropriate conditional branch with the rest of the schema
*/
Expand Down Expand Up @@ -764,6 +824,18 @@ export function retrieveSchema(schema, rootSchema = {}, formData = {}) {
return resolvedSchemaWithoutAllOf;
}
}

const hasPatternProperties =
resolvedSchema.hasOwnProperty("patternProperties") &&
Object.keys(resolvedSchema.patternProperties).length > 0;
if (hasPatternProperties) {
resolvedSchema = stubExistingPatternProperties(
resolvedSchema,
rootSchema,
formData
);
}

const hasAdditionalProperties =
resolvedSchema.hasOwnProperty("additionalProperties") &&
resolvedSchema.additionalProperties !== false;
Expand Down Expand Up @@ -1329,3 +1401,18 @@ export function schemaRequiresTrueValue(schema) {

return false;
}

export function isAddedProperty(property) {
return property.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG) || property.hasOwnProperty(PATTERN_PROPERTY);
}

export function uiSchemaForProperty(name, properties, uiSchema) {
const property = properties[name];

if (property.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG)) {
return uiSchema.additionalProperties;
} else if (property.hasOwnProperty(PATTERN_PROPERTY)) {
return uiSchema.patternProperties ? uiSchema.patternProperties[property[PATTERN_PROPERTY]] : undefined;
}
return uiSchema[name];
}
2 changes: 2 additions & 0 deletions packages/playground/src/samples/index.js
Expand Up @@ -22,6 +22,7 @@ import alternatives from "./alternatives";
import propertyDependencies from "./propertyDependencies";
import schemaDependencies from "./schemaDependencies";
import additionalProperties from "./additionalProperties";
import patternProperties from "./patternProperties";
import nullable from "./nullable";
import nullField from "./null";
import errorSchema from "./errorSchema";
Expand Down Expand Up @@ -50,6 +51,7 @@ export const samples = {
"Property dependencies": propertyDependencies,
"Schema dependencies": schemaDependencies,
"Additional Properties": additionalProperties,
"Pattern Properties": patternProperties,
"Any Of": anyOf,
"One Of": oneOf,
"All Of": allOf,
Expand Down
38 changes: 38 additions & 0 deletions packages/playground/src/samples/patternProperties.js
@@ -0,0 +1,38 @@
module.exports = {
schema: {
title: "A customizable registration form",
description: "A simple form with pattern properties example.",
type: "object",
required: ["firstName", "lastName"],
patternProperties: {
"^str_": {
type: "string",
},
"^num_": {
type: "number",
},
},
properties: {
firstName: {
type: "string",
title: "First name",
},
lastName: {
type: "string",
title: "Last name",
},
},
},
uiSchema: {
firstName: {
"ui:autofocus": true,
"ui:emptyValue": "",
},
},
formData: {
firstName: "Chuck",
lastName: "Norris",
str_assKickCount: "infinity",
num_assKickedCount: 0,
},
};

0 comments on commit 523c360

Please sign in to comment.