Skip to content
KajayDocs
CustomizeTypeScript SDKReactextension

Custom question types and renderers

Register one metadata-driven model and render it through a cloned public renderer registry.

Register metadata first

A custom question begins as a registry class with a parent, factory, properties, and any child collections. That one registration drives parsing, serialization, schema generation, and the Creator property grid.

Model and renderer registration
import { MetadataRegistry, Question, registerBuiltInTypes } from '@kajay/core';
import {
  defaultPageElementRenderers,
  QuestionErrors,
  QuestionTitleContent,
  questionErrorId,
  questionId,
  useIdScope,
  useQuestionValue,
} from '@kajay/react';

class MoodQuestion extends Question {
  override get type(): string { return 'mood'; }
}

const registry = new MetadataRegistry();
registerBuiltInTypes(registry);
registry.addClass({
  name: 'mood',
  parent: 'question',
  create: () => new MoodQuestion(),
  properties: [{ name: 'scale', type: 'number', defaultValue: 5 }],
});

const renderers = defaultPageElementRenderers.clone();
renderers.registerQuestion('mood', function MoodRenderer({ survey, question }) {
  const scope = useIdScope();
  const value = useQuestionValue(survey, question);
  const id = questionId(question, scope);
  const errors = questionErrorId(question, scope);
  return (
    <section>
      <label htmlFor={id}><QuestionTitleContent question={question} /></label>
      <QuestionErrors survey={survey} question={question} at="top" id={errors} />
      <input
        id={id}
        type="range"
        value={Number(value ?? 0)}
        aria-describedby={question.hasErrors ? errors : undefined}
        onChange={(event) => { question.value = Number(event.target.value); }}
      />
      <QuestionErrors survey={survey} question={question} at="bottom" id={errors} />
    </section>
  );
});

const { survey } = parseSurvey(definition, { registry });
<Survey model={survey} renderers={renderers} />

Clone the renderer registry

Clone defaultPageElementRenderers for one consumer and register the new renderer there. Do not mutate global defaults or import renderer internals.

Add properties to existing types

Use addProperty when every question or one existing class needs host metadata. Pass the same registry to runtime and Creator so parsing, serialization, and authoring agree.

A host property on every question
import {
  MetadataRegistry,
  parseSurvey,
  registerBuiltInTypes,
} from '@kajay/core';
import { CreatorWorkspace } from '@kajay/creator-core';

const registry = new MetadataRegistry();
registerBuiltInTypes(registry);

registry.addProperty('question', {
  name: 'helpUrl',
  type: 'string',
  description: 'A link to guidance for this question.',
});

// Use this same registry everywhere the definition travels.
const { survey } = parseSurvey(definition, { registry });
const workspace = new CreatorWorkspace({ definition, registry });

Render the question contract

  • Use exported title, error, id-scope, and component helpers rather than inventing parallel semantics.
  • Write through the question model so events, logic, read-only state, and validation remain coherent.
  • Add contract, unit, and real-browser proofs for new observable behavior.