Custom renderer not getting parent's renderers for jsonforms-outlet - Angular

I am making a custom renderer, and using jsonforms-outlet to display an “inner” form alongside the custom renderer. Using this, I keep getting No applicable renderer found! on renderers that are included in the parent component’s renderer list.

The instance of my custom renderer has a jsonFormsService object that does contain the renderers, but for some reason it doesn’t get passed into the jsonforms-outlet.

One workaround that I tried was the use jsonforms rather than the outlet, but this kept causing Maximum call stack size exceeded whenever trying to load the jsonform.

Custom renderer:

import { ChangeDetectorRef, Component } from "@angular/core";
import { JsonFormsAngularService, JsonFormsControl } from "@jsonforms/angular";
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-outlet
          *ngIf="shouldRenderJsonForms"
          [schema]="this.selectedOption"
        ></jsonforms-outlet>
    </div>
  `,
})
export class oneOfRenderer extends JsonFormsControl {
  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() {
    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;

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

    if (newSelectedOption !== this.selectedOption) {
      this.selectedOption = newSelectedOption;
      this.cdr.detectChanges();
    }

    this.shouldRenderJsonForms = true;
  }

  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;
  }
}

Parent component:

import { Component } from "@angular/core";
import { angularMaterialRenderers } from "@jsonforms/angular-material";
import { rankWith, schemaMatches } from "@jsonforms/core";
import schemaAsset from "../assets/schema.json";
import { oneOfRenderer } from "./oneOf-renderer";

@Component({
  selector: "app-root",
  templateUrl: `<div style="text-align:center">
  </div>
    <jsonforms
      [data]="data"
      [schema]="schema"
      [renderers]="renderers"
    ></jsonforms>
  `,
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  renderers = [
    ...angularMaterialRenderers,
    {
      renderer: oneOfRenderer,
      tester: rankWith(
        3,
        schemaMatches((schema) => schema.hasOwnProperty("oneOf"))
      ),
    },
  ];

  schema = schemaAsset;

  data = {};

  constructor() {}
}

Hi @anasisma,

Dispatching back to JSON Forms requires three data points:

  • The UI Schema element to render
  • The schema, fitting to the UI Schema element, i.e. which will be used to resolve the UI Schema
  • The current data path

You can see this in the JsonFormsOutlet component.

You can hand these over directly as props or as a combined prop called renderProps.

In your renderer you are currently only handing over the schema which is why the dispatching fails afterwards.

Conceptually all renderer sets behave in the same way (as they leverage the same core utilities), so feel free to browse the React or Vue code to see how the oneOf renderers were implemented there.