Skip to content

Commit

Permalink
Adapt Scopable and add Scoped, Labelable and Labeled interfaces
Browse files Browse the repository at this point in the history
Adjusts the 'scope' attribute of 'Scopable' to be optional as indicated by the interface
name. Adds new 'Scoped', 'Labelable' and 'Labeled' interfaces. Also adds type guards for
all mentioned interfaces.

Adapts 'composeWithUi' to being able to handle optional scopes via the adapted 'Scopable'
interface.

The new interfaces and type guards can be used to for example handle unknown UI Schemas
in a more convenient fashion.
  • Loading branch information
wienczny committed Jun 21, 2022
1 parent 31972a5 commit ddd4315
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 40 deletions.
6 changes: 6 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ There should not be any behavior changes.
All React Material class components were refactored to functional components.
Please check whether you extended any of our base renderers in your adaptation.

### Scopable interface change

The `scope` attribute in `Scopable` is now optional.
Use `Scoped` instead for non optional scopes.
The utility function `fromScopable` was renamed to `fromScoped` accordingly.

### Localization of Date Picker in Angular Material

Date Picker in Angular Material will use the global configuration of your Angular Material application.
Expand Down
75 changes: 49 additions & 26 deletions packages/core/src/models/uischema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,44 @@ import { JsonSchema } from './jsonSchema';

/**
* Interface for describing an UI schema element that is referencing
* a subschema. The value of the scope must be a JSON Pointer.
* a subschema. The value of the scope may be a JSON Pointer.
*/
export interface Scopable {
/**
* The scope that determines to which part this element should be bound to.
*/
scope?: string;
}

/**
* Interface for describing an UI schema element that is referencing
* a subschema. The value of the scope must be a JSON Pointer.
*/
export interface Scoped extends Scopable {
/**
* The scope that determines to which part this element should be bound to.
*/
scope: string;
}

/**
* Interface for describing an UI schema element that may be labeled.
*/
export interface Lableable<T = string> {
/**
* Label for UI schema element.
*/
label?: string|T;
}

/**
* Interface for describing an UI schema element that is labeled.
*/
export interface Labeled<T = string> extends Lableable<T> {
label: string | T;
}

/*
* Interface for describing an UI schema element that can provide an internationalization base key.
* If defined, this key is suffixed to derive applicable message keys for the UI schema element.
* For example, such suffixes are `.label` or `.description` to derive the corresponding message keys for a control element.
Expand Down Expand Up @@ -96,7 +124,7 @@ export interface Condition {
/**
* A leaf condition.
*/
export interface LeafCondition extends Condition, Scopable {
export interface LeafCondition extends Condition, Scoped {
type: 'LEAF';

/**
Expand All @@ -105,7 +133,7 @@ export interface LeafCondition extends Condition, Scopable {
expectedValue: any;
}

export interface SchemaBasedCondition extends Condition, Scopable {
export interface SchemaBasedCondition extends Condition, Scoped {
schema: JsonSchema;
}

Expand Down Expand Up @@ -179,12 +207,8 @@ export interface HorizontalLayout extends Layout {
* A group resembles a vertical layout, but additionally might have a label.
* This layout is useful when grouping different elements by a certain criteria.
*/
export interface GroupLayout extends Layout {
export interface GroupLayout extends Layout, Lableable {
type: 'Group';
/**
* The label of this group layout.
*/
label?: string;
}

/**
Expand Down Expand Up @@ -216,49 +240,48 @@ export interface LabelElement extends UISchemaElement {
* A control element. The scope property of the control determines
* to which part of the schema the control should be bound.
*/
export interface ControlElement extends UISchemaElement, Scopable, Internationalizable {
export interface ControlElement extends UISchemaElement, Scoped, Lableable<string | boolean | LabelDescription>, Internationalizable {
type: 'Control';
/**
* An optional label that will be associated with the control
*/
label?: string | boolean | LabelDescription;
}

/**
* The category layout.
*/
export interface Category extends Layout {
export interface Category extends Layout, Labeled {
type: 'Category';
/**
* The label associated with this category layout.
*/
label: string;
}

/**
* The categorization element, which may have children elements.
* A child element may either be itself a Categorization or a Category, hence
* the categorization element can be used to represent recursive structures like trees.
*/
export interface Categorization extends UISchemaElement {
export interface Categorization extends UISchemaElement, Labeled {
type: 'Categorization';
/**
* The label of this categorization.
*/
label: string;
/**
* The child elements of this categorization which are either of type
* {@link Category} or {@link Categorization}.
*/
elements: (Category | Categorization)[];
}

export const isInternationalized = (element: unknown): element is Required<Internationalizable> => {
return typeof element === 'object' && element !== null && typeof (element as Internationalizable).i18n === 'string';
}
export const isInternationalized = (element: unknown): element is Required<Internationalizable> =>
typeof element === 'object' && element !== null && typeof (element as Internationalizable).i18n === 'string';

export const isGroup = (layout: Layout): layout is GroupLayout =>
layout.type === 'Group';

export const isLayout = (uischema: UISchemaElement): uischema is Layout =>
(uischema as Layout).elements !== undefined;

export const isScopable = (obj: unknown): obj is Scopable =>
obj && typeof obj === 'object';

export const isScoped = (obj: unknown): obj is Scoped =>
isScopable(obj) && typeof obj.scope === 'string';

export const isLabelable = (obj: unknown): obj is Lableable =>
obj && typeof obj === 'object';

export const isLabeled = (obj: unknown): obj is Labeled =>
isLabelable(obj) && ['string', 'object'].includes(typeof obj.label);
14 changes: 9 additions & 5 deletions packages/core/src/util/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import isEmpty from 'lodash/isEmpty';
import range from 'lodash/range';
import { Scopable } from '../models';
import { isScoped, Scopable } from '../models';

export const compose = (path1: string, path2: string) => {
let p1 = path1;
Expand Down Expand Up @@ -81,13 +81,17 @@ export const toDataPath = (schemaPath: string): string => {
};

export const composeWithUi = (scopableUi: Scopable, path: string): string => {
if (!isScoped(scopableUi)) {
return path ?? '';
}

const segments = toDataPathSegments(scopableUi.scope);

if (isEmpty(segments) && path === undefined) {
return '';
if (isEmpty(segments)) {
return path ?? '';
}

return isEmpty(segments) ? path : compose(path, segments.join('.'));
return compose(path, segments.join('.'));
};

/**
Expand All @@ -99,4 +103,4 @@ export const encode = (segment: string) => segment?.replace(/~/g, '~0').replace(
/**
* Decodes a given JSON Pointer segment to its "normal" representation
*/
export const decode = (pointerSegment: string) => pointerSegment?.replace(/~1/g, '/').replace(/~0/, '~');
export const decode = (pointerSegment: string) => pointerSegment?.replace(/~1/g, '/').replace(/~0/, '~');
2 changes: 1 addition & 1 deletion packages/core/src/util/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import has from 'lodash/has';
import {
AndCondition,
Condition,
JsonSchema,
LeafCondition,
OrCondition,
RuleEffect,
Expand All @@ -39,7 +40,6 @@ import { composeWithUi } from './path';
import Ajv from 'ajv';
import { getAjv } from '../reducers';
import { JsonFormsState } from '../store';
import { JsonSchema } from '../models/jsonSchema';

const isOrCondition = (condition: Condition): condition is OrCondition =>
condition.type === 'OR';
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import isEmpty from 'lodash/isEmpty';
import isArray from 'lodash/isArray';
import includes from 'lodash/includes';
import find from 'lodash/find';
import { JsonSchema, Scopable, UISchemaElement } from '..';
import { JsonSchema, Scoped, UISchemaElement } from '..';
import { resolveData, resolveSchema } from './resolvers';
import { composePaths, toDataPathSegments } from './path';
import { isEnabled, isVisible } from './runtime';
Expand Down Expand Up @@ -56,8 +56,8 @@ export const hasType = (jsonSchema: JsonSchema, expected: string): boolean => {
};

/**
* Derives the type of the jsonSchema element
*/
* Derives the type of the jsonSchema element
*/
export const deriveTypes = (jsonSchema: JsonSchema): string[] => {
if (isEmpty(jsonSchema)) {
return [];
Expand Down Expand Up @@ -93,8 +93,8 @@ export const deriveTypes = (jsonSchema: JsonSchema): string[] => {
};

/**
* Convenience wrapper around resolveData and resolveSchema.
*/
* Convenience wrapper around resolveData and resolveSchema.
*/
export const Resolve: {
schema(
schema: JsonSchema,
Expand All @@ -108,18 +108,18 @@ export const Resolve: {
};

// Paths --
const fromScopable = (scopable: Scopable) =>
const fromScoped = (scopable: Scoped): string =>
toDataPathSegments(scopable.scope).join('.');

export const Paths = {
compose: composePaths,
fromScopable
fromScoped
};

// Runtime --
export const Runtime = {
isEnabled(uischema: UISchemaElement, data: any, ajv: Ajv): boolean {
return isEnabled(uischema, data,undefined, ajv);
return isEnabled(uischema, data, undefined, ajv);
},
isVisible(uischema: UISchemaElement, data: any, ajv: Ajv): boolean {
return isVisible(uischema, data, undefined, ajv);
Expand Down

0 comments on commit ddd4315

Please sign in to comment.