Skip to main content

Introduction

This documentation outlines a Dynamic Component System built in Vue, which allows you to render UI components ( including PrimeVue components) based on JSON or object-based configurations. Through these configurations, you can bind state, define events, and wire up actions (e.g. fetching data).

This is especially useful when you want to drive parts of your UI from a database, an API, or a CMS. You can build flexible pages or forms by referencing a set of rules or definitions rather than hard-coding every component.


Overview

  1. DynamicComponent.vue:

    • A core component that takes a config, globalState, and actions as props.
    • It automatically decides which Vue component to render (for example, a PrimeVue Button or a simple div) based on the configuration.
    • It also handles prop mappings, model bindings, translations (t()), and event handlers.
  2. DynamicRenderer.vue:

    • A higher-level component that:
      • Accepts a page-level config (e.g., "pageTitle", "components", "dataQueries").
      • Fetches data (if specified) and places it into a reactive state.
      • Iterates over each component in the config and renders them via the DynamicComponent.

The configuration typescript classes look like this:

export interface DataQuery {
url: string;
method?: string;
target: string;
}

export interface DynamicComponentConfig {
type: string;
props?: Record<string, any>;
events?: Record<string, string | ((...args: any[]) => void)>;
children?: DynamicComponentConfig[];
slots?: Record<string, DynamicComponentConfig[]>;
}

export interface PageConfig {
id: string;
pageTitle?: string;
dataQueries?: DataQuery[];
actions?: Record<string, unknown>;
components: DynamicComponentConfig[];
}

while PageConfig is the highest possible definition. It is required to match this pattern. These types can be used in configurations to assure the pattern is followed.


How It Works

  1. config.type:
    Determines which component is rendered. For example, a config like:

    {
    "type": "prime:button",
    "props": {
    "label": "Click Me"
    }
    }

    will load a PrimeVue Button component dynamically.

  2. config.props The props object is passed to the resolved component. Some props might be dynamically bound to values in globalState. For example:

    {
    "type": "prime:inputtext",
    "props": {
    "model": "userName"
    }
    }

    If globalState.userName changes, the input updates automatically.

  3. config.events:

    • Lets you define how to react to component events (e.g., click, onHide, etc.).
    • Each event can reference a function or a named action. For example:
      {
      "type": "prime:button",
      "props": {
      "label": "Confirm"
      },
      "events": {
      "click": "showConfirmationDialog"
      }
      }
      This looks up showConfirmationDialog in your actions object and calls it.
  4. config.slots (optional):

    • If the component supports named slots, you can nest entire configurations in them. For example, for a prime:dialog:
      {
      "type": "prime:dialog",
      "props": {
      "visible": "confirmationDialog"
      },
      "slots": {
      "footer": [
      {
      "type": "prime:button",
      "props": {
      "label": "Close"
      },
      "events": {
      "click": "closeDialog"
      }
      }
      ]
      }
      }
  5. config.children (alternative to slots):

    • For simpler scenarios, you can define direct children in a children array. For example:
      {
      "type": "div",
      "children": [
      {
      "type": "h1",
      "props": {
      "class": "title"
      },
      "children": [
      {
      "type": "text",
      "props": { "value": "Welcome!" }
      }
      ]
      }
      ]
      }
  6. Actions & Global State:

    • actions is an object containing either functions or descriptors that can be called from events.
    • globalState is a single source of truth for your dynamic page, storing user input and fetched data.

Example Configuration

Here’s a hypothetical configuration object that you might pass into DynamicRenderer.vue. This is just an example to show how you might structure a typical dynamic page:

{
"pageTitle": "home.dashboardTitle",
"dataQueries": [
{
"url": "/api/user",
"method": "GET",
"target": "userData"
}
],
"actions": {
"showConfirmationDialog": {
"type": "fetch",
"url": "/api/confirm",
"method": "POST",
"payloadKey": "userData"
},
"closeDialog": (state) => {
state.confirmationDialog = false;
}
},
"components": [
{
"type": "h2",
"props": {
"class": "text-xl",
"value": "t(greetings.title)"
}
},
{
"type": "prime:button",
"props": {
"label": "t(actions.confirm)",
"class": "p-button-success"
},
"events": {
"click": "showConfirmationDialog"
}
},
{
"type": "prime:dialog",
"props": {
"visible": "confirmationDialog",
"header": "t(dialog.header)"
},
"slots": {
"footer": [
{
"type": "prime:button",
"props": { "label": "Close" },
"events": { "click": "closeDialog" }
}
]
}
}
]
}

What’s Happening Here?

  • pageTitle: Used by DynamicRenderer to display a heading ($t(config.pageTitle)).
  • dataQueries: The system will GET /api/user on load, storing the result into state.userData.
  • actions:
    • showConfirmationDialog is an action that performs a POST to /api/confirm, sending state.userData as the request body.
    • closeDialog is a simple function that toggles off confirmationDialog.
  • components:
    • A header <h2> that loads a translation key.
    • A PrimeVue Button that triggers showConfirmationDialog.
    • A PrimeVue Dialog that references confirmationDialog from state.

Integrating in Your Vue App

  1. Registration
    Ensure DynamicComponent.vue and DynamicRenderer.vue are recognized by your Nuxt or Vue 3 application.

    • If using Nuxt, place them in the appropriate /components directory.
    • If using standard Vue, import and register them in your main app or in a parent component.
  2. Usage

    <template>
    <DynamicRenderer :config="pageConfig" />
    </template>

    <script>
    import pageConfig from "@/your/configs/page.json";
    import DynamicRenderer from "@/components/DynamicRenderer.vue";

    export default {
    components: { DynamicRenderer },
    setup() {
    // or define it in data() if using Options API
    return { pageConfig };
    }
    }
    </script>
  3. Translation (optional)
    If you have i18n set up, t(home.dashboardTitle) will be replaced with the localized string.
    You can embed translations in config strings with t(your.translation.key).


Final Thoughts

  • Why: Offload UI definitions to your database or other configurations for ultimate flexibility.
  • When: You need a dynamic form or you want users (or admin panels) to specify how the UI should look/behave without redeploying code.
  • Extend: You can extend primeVueMapping or any other mapping so your config can render specialized or custom components.

Reusing JSON for Rendering

Overview

You can define reusable data structures in a separate file (e.g., config.ts in your server or frontend). These structures—like arrays of components, helper functions for generating buttons/links, or entire page configurations—can then be consumed by your Dynamic Renderer to produce fully dynamic UIs. This approach reduces boilerplate, promotes consistency, and keeps your code organized.


Suppose you have helper functions in config.ts to build JSON "blueprints" for certain UI elements:

export const createButton: any = (label: string,
rounded: boolean = true,
text: boolean = false,
outline: boolean = false): DynamicComponentConfig => {
return {
"type": "prime:button",
"props": {
"label": label,
"rounded": rounded,
"text": text,
"outline": outline,
"class": ""
}
}
};

export const createLink: any = (to: string,
label: string,
rounded: boolean = true,
text: boolean = false,
outline: boolean = false): DynamicComponentConfig => {
return {
"type": "nuxt-link",
"props": {
"to": to
},
"children": [
createButton(label, rounded, text, outline)
]
}
};

These functions produce JSON objects (which match the format expected by the Dynamic Component or Dynamic Renderer). For instance, createButton("Save") returns a JSON object describing a PrimeVue Button that the renderer can display.


Defining Page Configurations

You can then organize entire pages by combining these reusable snippets:

const pages = [
{
"id": "dashboard",
"components": [
{
"type": "div",
"props": { "class": "w-full" },
"children": [
{
"type": "prime:card",
"props": { "class": "glass-card p-4" },
"slots": {
"header": [
// A helper that creates HTML content
// e.g., a function createHtml() returning {type: 'html', props: {...}}
],
"content": [
// Another helper that adds descriptive text
],
"footer": [
// Reuse createLink to add a link with a button inside
createLink("/entities", "t(action.open)")
]
}
}
]
}
]
}
];

Here, each createXyz helper is invoked to produce the JSON schema for an element. You might have 2–3 different pages, each pulling from the same library of helper functions (like createButton, createLink, etc.).


Benefits of This Approach

  1. Consistency: By writing helper functions (like createButton or createLink), you ensure that buttons, links, or cards appear and behave the same throughout your app.
  2. Maintainability: If you need to tweak the styling or functionality of a button, you only update the helper function—no need to rewrite multiple code snippets.
  3. Scalability: As your app grows, you can add more helper functions (like createInputField, createForm, etc.) or extend existing ones without cluttering your core logic.

Summary

  • Store your dynamic UI blueprints (button shapes, link shapes, page layouts) in a single file (e.g., config.ts).
  • Generate them with small, reusable functions (createButton, createLink, createHtml, etc.).
  • Combine them into larger page definitions (pages array), referencing your helper functions.
  • Render them through DynamicRenderer, which orchestrates how each piece is turned into an actual Vue/PrimeVue component.

By reusing JSON for rendering, you get a clean separation of data/structure from the rendering logic, making it easier to maintain and expand your app.


Additional Tips

  1. Validation:
    If you attach props.validators and define a validateForm action, the system can automatically track errors in globalState._formErrors.

  2. Styling:

    • Pass CSS classes or inline styles in the config ("props": { "class": "my-class", "style": "color: red;" }).
    • Or rely on your existing Tailwind/utility classes in the config.
  3. Slots vs. Children:

    • Use slots if the base component supports named slots (e.g. <footer> in PrimeVue Dialog).
    • Use children if you simply want nested elements in the same default slot.

That’s it! With this approach, you can define entire pages, forms, or dynamic sections with just JSON or object-based definitions and leverage all the power of Vue + PrimeVue behind the scenes.