Incorrect schema passed into custom renderer when type is array - Angular

I am making a custom angular renderer for oneOf, and it works fine with a single oneOf object. However, if I make the schema contain an array of oneOf objects, the schema that gets passed into the renderer becomes incorrect.

For example, with the following schema, the definitions section just disappears, and then the schema is unable to parse the references in the oneOf. When I look into the renderer instance, I can see that it has a jsonFormsService object that contains the correct schema, but for some reason the instance is using its own version of the schema.

Schema:

{
  "definitions": {
    "address": {
      "type": "object",
      "title": "Address",
      "properties": {
        "street_address": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        }
      },
      "required": ["street_address", "city", "state"]
    },
    "user": {
      "type": "object",
      "title": "User",
      "properties": {
        "name": {
          "type": "string"
        },
        "mail": {
          "type": "string"
        }
      },
      "required": ["name", "mail"]
    }
  },
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "addressOrUser": {
        "oneOf": [
          {
            "$ref": "#/definitions/address"
          },
          {
            "$ref": "#/definitions/user"
          }
        ]
      }
    }
  }
}

Here is the renderer:

import { ChangeDetectorRef, Component } from "@angular/core";
import { JsonFormsAngularService, JsonFormsControl } from "@jsonforms/angular";
import { angularMaterialRenderers } from "@jsonforms/angular-material";
import { JsonSchema, resolveSchema } from "@jsonforms/core";

@Component({
  selector: "oneOf-renderer",
  template: `
    <div>
      <label>{{ label }}</label>
      <select (change)="onOptionChange($event)">
        <option *ngFor="let option of options" [value]="option.title">
          {{ option.title }}
        </option>
      </select>
      <jsonforms
        *ngIf="shouldRenderJsonForms"
        [data]="data"
        [schema]="selectedOption"
        [renderers]="this.renderers"
      ></jsonforms>
    </div>
  `,
})
export class oneOfRenderer extends JsonFormsControl {
  renderers = [...angularMaterialRenderers];

  options: any;
  selectedOption: any;

  shouldRenderJsonForms = false; // Used to mount/unmount the json form html because it doesn't refresh when a new schema is provided

  constructor(jsonFormsService: JsonFormsAngularService, private cdr: ChangeDetectorRef) {
    super(jsonFormsService);
  }

  ngOnInit() {
    console.log(this);
    // this.options = this.extractOptions(this.schema);

    // if (this.options.length > 0) {
    //   this.selectedOption = this.options[0];
    // }

    // this.shouldRenderJsonForms = true;
  }

  onOptionChange(event: any) {
    this.shouldRenderJsonForms = false;

    this.selectedOption = this.options.find(
      (option) => option.title === event.target.value
    );

    console.log(event.target);

    this.cdr.detectChanges();
    this.shouldRenderJsonForms = true;
  }

  extractOptions(schema: JsonSchema) {
    console.log(schema);
    for (const key in schema.properties) {
      if (schema.properties.hasOwnProperty(key)) {
        const property = schema.properties[key];

        if (property.oneOf) {
          const refs = property.oneOf.map((option: any) => option);
          console.log(refs);

          const options = [];

          for (const key in refs) {
            console.log(refs[key]["$ref"]);
            options.push(resolveSchema(schema, refs[key]["$ref"], schema));
          }

          console.log(options);
          return options;
        }
      }
    }
  }
}

Hi @anasisma,

We always hand over the resolved (sub)schema as schema to the renderers as usually this contains sufficient information for rendering. In case a renderer encounters a $ref and they want to resolve it, they can use either the form-wide whole schema from the service, as you discovered yourself, or just access the (same) rootSchema which we also hand over.

1 Like

Hey @sdirix

I was trying to implement your suggestion, but I’m finding that my renderer instance doesn’t receive the data, path, rootSchema and many other properties from the parent. I’m guessing I set something up wrong, but not sure what. Any help would be appreciated.

Here is the updated renderer:

import { ChangeDetectorRef, Component } from "@angular/core";
import { JsonFormsAngularService, JsonFormsControl } from "@jsonforms/angular";
import { Generate, JsonSchema, resolveSchema } from "@jsonforms/core";

@Component({
  selector: "oneOf-renderer",
  template: `
    <div>
      <label>{{ label }}</label>

      <select (change)="onOptionChange($event)">
        <option *ngFor="let option of options" [value]="option.title">
          {{ option.title }}
        </option>
      </select>
        <jsonforms-outlet
          [schema]="this.selectedOption"
          [uischema]="this.selectedUi"
		  [path]="path"
        ></jsonforms-outlet>
    </div>
  `,
})
export class oneOfRenderer extends JsonFormsControl {
  options: any[];
  allUiSchemas: { [key: string]: any } = {};
  selectedOption: JsonSchema;
  selectedUi: JsonSchema;

  constructor(jsonFormsService: JsonFormsAngularService, private cdr: ChangeDetectorRef) {
    super(jsonFormsService);
  }

  ngOnInit() {
    this.options = this.extractOptions(this.schema);

    if (this.options.length > 0) {
      this.selectedOption = this.options[0];
      this.selectedUi = Generate.uiSchema(this.selectedOption);
    }
	console.log(this)
  }

  onOptionChange(event: any) {

    const selectedTitle = event.target.value;

    const newSelectedOption = this.options.find(
      (option) => option.title === selectedTitle
    );

    if (newSelectedOption !== this.selectedOption) {

      this.selectedOption = newSelectedOption;

      if (!this.allUiSchemas[selectedTitle]) {
        this.allUiSchemas[selectedTitle] = Generate.uiSchema(this.selectedOption);
      }

      this.selectedUi = this.allUiSchemas[selectedTitle];

      this.cdr.detectChanges();
	  this.jsonFormsService.refresh()
    }
  }

  extractOptions(schema: JsonSchema) {
    const options = [];

    for (const key in schema.properties) {
      const property = schema.properties[key];

      if (property.oneOf) {
        property.oneOf.forEach((option: any) => {
          options.push(resolveSchema(schema, option["$ref"], schema));
        });

        break;
      }
    }

    return options;
  }
}

Here is a screenshot of an example instance of the renderer:

Hi @anasisma,

The mapper logic is executed in ngOnInit, see here.

You are overriding ngOnInit without calling super(), therefore this is then lost for you.

1 Like