surveyjs / survey-library

Free JavaScript form builder library with integration for React, Angular, Vue, jQuery, and Knockout.
https://surveyjs.io/form-library
MIT License
4.21k stars 815 forks source link

Question: Register custom Vue component as widget or question type #4665

Open metaturso opened 2 years ago

metaturso commented 2 years ago

Are you requesting a feature, reporting a bug or asking a question?

Asking a question: What is the correct approach to declare a custom Vue component as a new question type? I've tried custom widgets and registering a native question type, but I got stuck.

Desiderata: I want to create a custom file upload question based on VueFileAgent component. [^1]

What is the current behavior?

Custom Widget handler can't render Vue components.

The SurveyJS documentation states in Create Custom Widget that "[...] you want to create a new Custom Widget [to] create a new question, typically using an existing third-party JavaScript widget/library.".

So far I've seen examples of overriding an existing question type (empty or comment) with a short snippet of HTML, for example:

  htmlTemplate: `<div>
    <div>
      <button onclick="document.execCommand('bold')">Bold</a>
      <button onclick="document.execCommand('italic')">Italic</a>
      <button onclick="document.execCommand('insertunorderedlist')">List</a>
    </div>
    <div class="widget_rich_editor" contenteditable=true style="height:200px"></div>
  </div>`,

This is approach is fine for simple form controls. But I can't see a way to scale it up to render a Vue component. So much so that I wonder if Widgets are the right approach to declaring a custom Vue component as a new question type?

Is the documentation failing to mention that I should pre-render the component into the htmlTemplate string? If not that, should I render a simple HTML element and attach the Vue component to that? For example, to me this seem like a plausible approach:

  htmlTemplate: `<div id="customQuestionAttachmentPoint" />`

Then again, I don't think this is the right way either. Using this template string falls apart with surveys that contain multiple instances of this custom question type.

I've concluded that a widget isn't the right way to declare a custom Vue component as a new question type. If you disagree, would you mind pointing me in the right direction? [^2]

Custom SurveyJS question type can't extend QuestionVue

My next attempt was to reverse-engineer and hook into the SurveyJS native question registration and resolution process.

I've created a minimal working example. Here's an excerpt:

// File: MyFirstSurvey.vue from the surveyjs/code-examples repository.
import FileAgentQuestion, {QuestionFileAgentModel} from "@/components/FileAgentQuestion.vue";

function registerFileAgentQuestion() {
  const FILE_AGENT_PARENT_TYPE = 'empty';
  const FILE_AGENT_QUESTION_TYPE = 'file-agent';

  Vue.component(`survey-${FILE_AGENT_QUESTION_TYPE}`, FileAgentQuestion);

  Serializer.addClass(FILE_AGENT_QUESTION_TYPE,[], () => {
    return new QuestionFileAgentModel(FILE_AGENT_QUESTION_TYPE);
  }, FILE_AGENT_PARENT_TYPE);
}

registerFileAgentQuestion();

const surveyJson = {
  elements: [
    {
      name: "question-id-2",
      title: "Upload picture",
      type: "file-agent"
    },
  ]
};

export default {
  name: 'MyFirstSurvey',
  // [...]
}

This works but it's far from a complete integration. There are a couple hurdles I couldn't overcome by myself.

Firstly, I couldn't declare my component class as class FileAgentQuestion extends QuestionVue<QuestionFileAgentModel> because I only have the QuestionVue class declaration, not an implementation. This type of error isn't obvious to me:

Uncaught TypeError: class heritage QuestionVue is not an object or null

I suppose that the runtime version of survey-core and survey-vue-ui don't include those implementation. Unless I'm doing something painfully wrong?

I'm using Vue.extend as a fall back, but I'd much rather extend one of the base VueQuestion components to avoid duplicating the setup and integration code. This is the biggest blocker. As far as I understand, I must extend a base question components to tie the class with the rest of SurveJS data-flow and make the new question configurable and its value accessible to the Survey instance.

Secondly, because the component is registered using strings, the import is marked as unused and trimmed during build. I guess I could import "FileAgentQuestion.vue" for side-effects in my entrypoint. Or import the symbols and register them in my entrypoint or root component so they count as one usage. I suppose that SurveyJS' build process bundles each question into the final artifact. I could configure my Webpack to do the same and get rid of the import altogether. Isn't it possible to register the type instead?

What do you suggest I do?

What is the expected behavior?

SurveyJS should have a mechanism to register a new question type that renders a custom Vue (or React, or Angular) component.

I'm not a fan of the custom widget approach. It's clunky and inflexible. I would like to declare my Vue component as close as possible to a "native" SurveyJS question type.

How would you reproduce the current behavior (if this is a bug)?

Sample repository.

Version used

[^1]: There are two reasons that I've intentionally avoided declaring my new question type as a renderAs variant of the native file question. [^2]: I can't include vue-template-compiler as a runtime dependency.

metaturso commented 1 year ago

@andrewtelnov Can you offer any advice on how to create and integrate custom question components that SurveyJS can load without using the widget feature?

No hand holding required, we only need a few pointers and we'll be happy to fill in the blanks. Thanks.

metaturso commented 1 year ago

Hi @andrewtelnov, following the official third-party React component integration guide, mutati mutandis and using surveyjs-library sources as a reference, I keep hitting a problem when extending either BaseVue or QuestionVue<T> imported with:

import { BaseVue, QuestionVue } from "survey-vue-ui"

export class CustomTypeA extends BaseVue {}
// or
export class CustomTypeB extends extends QuestionVue<CustomTypeBModel>

The problem manifests as a runtime error along the lines of:

Uncaught TypeError: class heritage survey_vue_ui__WEBPACK_IMPORTED_MODULE_4__.BaseVue is not an object or null
Uncaught TypeError: class heritage survey_vue_ui__WEBPACK_IMPORTED_MODULE_3__.QuestionVue is not an object or null

I suppose this is because the package doesn't include the full class declarations? If that's the problem, would it be possible to ship those declarations to enable third-party developers to better integrate SurveyJS with their VueJS applications?

The problem happens with survey-core=1.9.54 and survey-vue-ui=1.9.54.

@RomanTsukanov would it be possible to adapt the third-party component guide to cover Vue components integration?