Let's say I have a root standalone component that wishes to share some of its config ( viewerOptions in the example below) to all his children's
import { Component, OnInit, Input, signal, computed, ChangeDetectionStrategy } from '@angular/core';
// imports
import { CommonModule } from '@angular/common';
import { MatExpansionModule } from '@angular/material/expansion';
import {
CreateNodesComponent
} from "./common/index"
// services
import { SchemaResolutionService } from './services/schema-resolver';
import { JSVOptionsService } from "./services/jsv-options";
// Labels
import {
ErrorOccurredLabelComponent,
LoadingLabelComponent
} from "./labels/index";
// Types
import type { JSONSchema } from './types';
import type { IResolveOpts } from "@stoplight/json-ref-resolver/types"
import type { JSVOptions } from "./services/jsv-options";
type StatusType = "LOADING" | "ERROR" | "DONE";
@Component({
selector: 'ngx-json-schema-viewer',
standalone: true,
imports: [
CommonModule,
MatExpansionModule,
CreateNodesComponent,
ErrorOccurredLabelComponent,
LoadingLabelComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
JSVOptionsService
],
template: `
<!-- Error ... -->
<ng-container *ngIf="status() === 'ERROR'">
<div>
<labels-error-occurred [error]="error()!"/>
</div>
</ng-container>
<!-- Loading ... -->
<ng-container *ngIf="status() === 'LOADING'">
<labels-loading />
</ng-container>
<!-- Schema -->
<ng-container *ngIf="status() === 'DONE'">
<mat-accordion>
<mat-expansion-panel [(expanded)]="expanded">
<mat-expansion-panel-header>
<mat-panel-title>
<strong>
{{ getSchemaTitle }}
</strong>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<jse-common-create-nodes [schema]="resolvedSchema()!" />
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</ng-container>
`
})
export class NgxJsonSchemaViewerComponent implements OnInit {
@Input({ required: true }) schema: unknown;
@Input() resolverOptions?: IResolveOpts;
@Input() viewerOptions?: Partial<JSVOptions>;
expanded : boolean = true;
resolvedSchema = signal<JSONSchema | undefined>(undefined);
error = signal<Error | undefined>(undefined);
status = computed<StatusType>(() => {
if (this.error() !== undefined) {
return "ERROR";
} else if (this.resolvedSchema() === undefined) {
return "LOADING";
} else {
return "DONE";
}
});
constructor(
private schemaResolutionService: SchemaResolutionService,
private jsvOptionsService: JSVOptionsService,
) {}
ngOnInit(): void {
// If asked, apply user options
if (this.viewerOptions) {
this.jsvOptionsService.setOptions(this.viewerOptions);
}
// Perform the asynchronous schema resolution
this.schemaResolution();
}
private schemaResolution() {
this.schemaResolutionService
.resolveSchema(this.schema, this.resolverOptions)
.subscribe({
error: (err) => {
this.error.set(err);
this.resolvedSchema.set(undefined);
},
next: (result) => {
this.resolvedSchema.set(result);
this.error.set(undefined);
}
});
}
get getSchemaTitle() : string {
let schema = this.resolvedSchema()!;
if (typeof schema !== "boolean" && schema.title !== undefined) {
return schema.title;
}
return "Schema";
}
}
Config service is defined like that :
import { Injectable } from '@angular/core';
export type CheckKey =
| "nullable"
| "deprecated"
| "readOnly"
| "writeOnly"
| "enum"
| "stringLength"
| "objectProperties"
| "no-extra-properties"
| "arrayItems"
| "arrayContains"
| "no-extra-items"
| "number-range"
| "pattern"
| "multipleOf"
| "uniqueItems"
| "default"
| "const"
| "examples"
| "contentMediaType"
| "contentEncoding"
| "contentSchema"
export type JSVOptions = {
/**
* Should we display "examples" ?
* @default false
*/
showExamples: boolean
/**
* To overwrite the order to display qualifier messages
* @default ["nullable","deprecated","readOnly","writeOnly","enum","stringLength","objectProperties","no-extra-properties","arrayItems","arrayContains","no-extra-items","number-range","pattern","multipleOf","uniqueItems","contentEncoding","contentMediaType","contentSchema","default","const","examples"]
*/
qualifierMessagesOrder: CheckKey[]
}
@Injectable({
providedIn: "root"
})
export class JSVOptionsService {
private options: JSVOptions;
constructor() {
this.options = {
showExamples: false,
qualifierMessagesOrder: [
"nullable",
"deprecated",
"readOnly",
"writeOnly",
"enum",
"stringLength",
"objectProperties",
"no-extra-properties",
"arrayItems",
"arrayContains",
"no-extra-items",
"number-range",
"pattern",
"multipleOf",
"uniqueItems",
"contentEncoding",
"contentMediaType",
"contentSchema",
"default",
"const",
"examples"
]
}
}
setOptions(userOptions?: Partial<JSVOptions>) {
this.options = {
...this.options,
...userOptions
}
}
getOptions(): JSVOptions {
return this.options;
}
}
Example of child component that uses it deep down the component tree :
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { JSVOptionsService } from '../services/jsv-options';
// qualifier messages
import {
ConstantComponent,
ArrayContainsNumberComponent,
ArrayNumberOfItemsComponent,
ArrayUniqueItemsComponent,
ContentEncodingComponent,
ContentMediaTypeComponent,
ContentSchemaComponent,
DefaultValueComponent,
DeprecatedComponent,
EnumComponent,
ExamplesComponent,
MultipleOfComponent,
NoExtraItemsComponent,
NoExtraPropertiesComponent,
NullableComponent,
NumberBoundsComponent,
ObjectPropertiesComponent,
PatternComponent,
ReadOnlyComponent,
StringLengthComponent,
WriteOnlyComponent
} from "./QualifierMessages/index";
// Types
import type { JSONSchema, JSONSchemaNS } from '../types';
import type { JSVOptions, CheckKey } from '../services/jsv-options';
@Component({
selector: 'qm-messages',
standalone: true,
imports: [
CommonModule,
ConstantComponent,
DefaultValueComponent,
EnumComponent,
ExamplesComponent,
ReadOnlyComponent,
WriteOnlyComponent,
ArrayUniqueItemsComponent,
DeprecatedComponent,
NullableComponent,
StringLengthComponent,
ObjectPropertiesComponent,
NoExtraPropertiesComponent,
ArrayNumberOfItemsComponent,
ArrayContainsNumberComponent,
NoExtraItemsComponent,
NumberBoundsComponent,
PatternComponent,
MultipleOfComponent,
ContentEncodingComponent,
ContentMediaTypeComponent,
ContentSchemaComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<ng-container *ngFor="let key of filteredQualifiers">
<ng-container [ngSwitch]="key">
<qm-constant *ngSwitchCase="'const'" [schema]="schema"/>
<qm-default-value *ngSwitchCase="'default'" [schema]="schema"/>
<qm-enum *ngSwitchCase="'enum'" [schema]="schema"/>
<qm-examples *ngSwitchCase="'examples'" [schema]="schema"/>
<qm-read-only *ngSwitchCase="'readOnly'" />
<qm-write-only *ngSwitchCase="'writeOnly'" />
<qm-array-unique-items *ngSwitchCase="'uniqueItems'" />
<qm-deprecated *ngSwitchCase="'deprecated'" />
<qm-nullable *ngSwitchCase="'nullable'" />
<qm-string-length *ngSwitchCase="'stringLength'" [schema]="schema" />
<qm-object-properties *ngSwitchCase="'objectProperties'" [schema]="schema" />
<qm-no-extra-properties *ngSwitchCase="'no-extra-properties'" />
<qm-array-number-of-items *ngSwitchCase="'arrayItems'" [schema]="schema" />
<qm-array-contains *ngSwitchCase="'arrayContains'" [schema]="schema" />
<qm-no-extra-items *ngSwitchCase="'no-extra-items'" />
<qm-number-bounds *ngSwitchCase="'number-range'" [schema]="schema" />
<qm-pattern *ngSwitchCase="'pattern'" [schema]="schema" />
<qm-multiple-of *ngSwitchCase="'multipleOf'" [schema]="schema" />
<qm-content-encoding *ngSwitchCase="'contentEncoding'" [schema]="schema" />
<qm-content-media-type *ngSwitchCase="'contentMediaType'" [schema]="schema" />
<qm-content-schema *ngSwitchCase="'contentSchema'" [schema]="typedAsJSONSchemaString" />
</ng-container>
</ng-container>
</div>
`
})
export class QualifierMessages {
@Input({ required: true }) schema!: Exclude<JSONSchema, true | false>;
constructor(private jsvOptionsService: JSVOptionsService) {}
get options(): JSVOptions {
return this.jsvOptionsService.getOptions();
}
get typedAsJSONSchemaString(): JSONSchemaNS.String {
return this.schema as JSONSchemaNS.String;
}
get filteredQualifiers(): CheckKey[] {
const qualifierMessagesOrder = this.options.qualifierMessagesOrder;
const filteredMessagesOrder = qualifierMessagesOrder.filter(qualifierKey => {
switch(qualifierKey) {
case 'const':
return this.schema.const !== undefined;
case 'default':
return this.schema.default !== undefined;
case 'enum':
return this.schema.enum !== undefined;
case 'examples':
return (this.options.showExamples || false) && this.schema.examples !== undefined;
case 'readOnly':
return this.schema.readOnly === true;
case 'writeOnly':
return this.schema.writeOnly === true;
case 'uniqueItems':
return this.schema.uniqueItems === true;
case 'deprecated':
return (this.schema as JSONSchemaNS.Object).deprecated === true;
case 'nullable':
return (this.schema as any).nullable === true;
case 'stringLength':
return this.schema.minLength !== undefined || this.schema.maxLength !== undefined;
case 'objectProperties':
return this.schema.minProperties !== undefined || this.schema.maxProperties !== undefined;
case 'no-extra-properties':
return this.schema.additionalProperties === false || (this.schema as JSONSchemaNS.Object).unevaluatedProperties === false;
case 'arrayItems':
return this.schema.minItems !== undefined || this.schema.maxItems !== undefined;
case 'arrayContains':
return (this.schema as JSONSchemaNS.Array).minContains !== undefined || (this.schema as JSONSchemaNS.Array).maxContains !== undefined;
case 'no-extra-items':
return (this.schema as JSONSchemaNS.Array).unevaluatedItems === false || this.schema.items === false || this.schema.additionalItems === false;
case 'number-range':
return this.schema.minimum !== undefined || this.schema.exclusiveMinimum !== undefined || this.schema.maximum !== undefined || this.schema.exclusiveMaximum !== undefined;
case 'pattern':
return this.schema.pattern !== undefined;
case 'multipleOf':
return this.schema.multipleOf !== undefined;
case 'contentMediaType':
return this.schema.contentMediaType !== undefined;
case 'contentEncoding':
return this.schema.contentEncoding !== undefined;
case 'contentSchema':
return (this.schema as JSONSchemaNS.String).contentSchema !== undefined;
}
});
// To debug easily in the future ;)
return filteredMessagesOrder;
}
}
Instead of getting something like :
I got a crash on Storybook : https://master--65174c82cd070b9998efd7f6.chromatic.com/?path=/story/viewer-generic-keywords--annotations . Could you enlight me where I was wrong ?
Thanks in advance