diff --git a/packages/docs/components/Notification.md b/packages/docs/components/Notification.md
index cf6a4a10a..c1b5f422e 100644
--- a/packages/docs/components/Notification.md
+++ b/packages/docs/components/Notification.md
@@ -50,7 +50,7 @@ It is designed to mimic the push notifications that have been popularized by mob
| icon | Icon name to use | string | - | |
| iconPack | Icon pack to use | string | `mdi`, `fa`, `fas and any other custom icon pack` |
notification: { iconSize: "large" } |
-| message | Message text (can contain HTML), unnecessary when default slot is used | string \| string[] | - | |
+| message | Message text, unnecessary when default slot is used | string | - | |
| override | Override existing theme classes completely | boolean | - | |
| position | Which position the notification will appear when programmatically | "bottom-left" \| "bottom-right" \| "bottom" \| "top-left" \| "top-right" \| "top" | `top-right`, `top`, `top-left`, `bottom-right`, `bottom`, `bottom-left` |
From config:
notification: { position: "top" } |
| type | Type (color) of the notification | string | `info`, `success`, `warning`, `danger` | |
diff --git a/packages/docs/documentation/composables.md b/packages/docs/documentation/composables.md
index 68250e657..885a3acac 100644
--- a/packages/docs/documentation/composables.md
+++ b/packages/docs/documentation/composables.md
@@ -172,13 +172,16 @@ type ProgrammaticExpose = {
## Component Programmatic {#programmatic}
Oruga comes with a component that is only available programmatically. This component can be used to mount **any** custom component programmatically, using the [Vue render function](https://vuejs.org/api/render-function.html#render-function-apis) and [Creating Vnodes](https://vuejs.org/guide/extras/render-function.html#render-function-recipes).
-The component works as follows: The programmatic component renders a wrapper component in a sperate shadow Vue instance. The separate Vue instance will have the same context object as the current one. The provided component is then rendered in a wrapper component that handles the Vue lifecycle events of the provided component.
-The rendered component is then extracted from the shadow Vue instance and placed into the target container of the real DOM instance.
-By closing the instance of the wrapper component, for example by calling `oruga.programmatic.close()` from outside, or by firing a `close` event from inside the provided component, the wrapper component and the shadow Vue instance will be destroyed and DOM will be cleaned up.
+The component works like this:
+The programmatic component creates a new, separate Vue instance, with a wrapper component as root element.
+The new Vue instance can be seen in the [Vue Devtools](https://devtools.vuejs.org/) with the name `ProgrammaticApp`.
+The separate Vue instance will have the same context object as the current one and will be rendered into a `div` in the target DOM container (by default, it is rendered into the `body` element).
+The provided component is then rendered into the wrapper component, which handles the Vue lifecycle events of the provided component.
+Closing the instance of the wrapper component, for example by calling `oruga.programmatic.close()` from outside, or by firing a `close` event from inside the provided component, will destroy the wrapper component and the new Vue instance, and clean up the DOM.
-> ***Note:*** When using the programmatic component, you may experience some DX issues if you run the Vue Devtools and inspect the programmatic component.
+> ***Note:*** For performance reasons, be careful not to open too many programmatic components at once, each of which will create a separate Vue instance.
-By adding this component using the main Oruga plugin or the dedicated `ComponentProgrammatic` plugin, the component adds an interface `programmatic` to the `useOruga()` composable and provides the `ComponentProgrammatic` object export, but it does not have a Vue component to mount directly.
+By adding this component using the main Oruga plugin or the dedicated `ComponentProgrammatic` plugin, the component adds an interface `programmatic` to the `useOruga()` composable and provides the `ComponentProgrammatic` object export, but it does not have a Oruga component.
```typescript
import { useOruga } from "@oruga-ui/oruga-next";
@@ -191,12 +194,11 @@ const slot = "My default slot content";
oruga.programmatic.open(
MyComponent,
{
- target: document.body, // target the component get rendered into
+ target: document.body, // target container the programmatic component get rendered into
+ appId: "programmatic-app", // HTML #id of the app div rendered into the target container
props: { ... }, // component specific props
onClose: (...args: unknown[]) => { ... }, // on close event handler
- },
- // component default slot render content
- slot,
+ }
);
```
@@ -209,16 +211,20 @@ type open = (
component: C,
/** render options */
options?: ProgrammaticOptions,
- /** default slot content */
- slot?: unknown
) => ProgrammaticExpose;
type ProgrammaticOptions = {
/**
- * The target specifies the element the component get rendered into - default is `document.body`.
+ * The target specifies the element the component get rendered into.
+ * @default `document.body`.
*/
target?: string | HTMLElement;
+ /**
+ * Specify the template `id` for the programmatic container element.
+ * @default `programmatic-app`
+ */
+ appId?: string;
/**
* Props to be binded to the injected component.
* Both attributes and properties can be used in props.
@@ -226,7 +232,7 @@ type ProgrammaticOptions = {
* `class` and `style` have the same object / array value support like in templates.
* Event listeners should be passed as onXxx.
*/
- props: ComponentProps,
+ props?: ComponentProps,
/**
* On component close event.
* This get called when the component emits `close` or the exposed `close` function get called.
diff --git a/packages/oruga/src/components/loading/useLoadingProgrammatic.ts b/packages/oruga/src/components/loading/useLoadingProgrammatic.ts
index 6d4006005..fe14209e2 100644
--- a/packages/oruga/src/components/loading/useLoadingProgrammatic.ts
+++ b/packages/oruga/src/components/loading/useLoadingProgrammatic.ts
@@ -20,11 +20,8 @@ declare module "../../index" {
const instances = new InstanceRegistry();
/** useLoadingProgrammatic composable options */
-export type LoadingProgrammaticOptions = Readonly<
- Omit
-> & {
- label?: string | Array;
-} & ProgrammaticComponentOptions;
+export type LoadingProgrammaticOptions = Readonly &
+ ProgrammaticComponentOptions;
const LoadingProgrammatic = {
/**
@@ -40,13 +37,6 @@ const LoadingProgrammatic = {
const _options: LoadingProgrammaticOptions =
typeof options === "string" ? { label: options } : options;
- let slot;
- // render content as slot when is an array
- if (Array.isArray(_options.label)) {
- slot = _options.label;
- delete _options.label;
- }
-
const componentProps: LoadingProps = {
active: true, // set the active default state to true
fullPage: false, // set the full page default state to false
@@ -54,17 +44,12 @@ const LoadingProgrammatic = {
};
// create programmatic component
- return ComponentProgrammatic.open(
- Loading,
- {
- instances, // custom programmatic instance registry
- target, // target the component get rendered into
- props: componentProps, // component specific props
- onClose: _options.onClose, // on close event handler
- },
- // component default slot render
- slot,
- );
+ return ComponentProgrammatic.open(Loading, {
+ instances, // custom programmatic instance registry
+ target, // target the component get rendered into
+ props: componentProps, // component specific props
+ onClose: _options.onClose, // on close event handler
+ });
},
/** Close the last registred instance in the loading programmatic instance registry. */
close(...args: unknown[]): void {
diff --git a/packages/oruga/src/components/modal/examples/programmatically.vue b/packages/oruga/src/components/modal/examples/programmatically.vue
index ad8e32219..0e8ee6983 100644
--- a/packages/oruga/src/components/modal/examples/programmatically.vue
+++ b/packages/oruga/src/components/modal/examples/programmatically.vue
@@ -6,6 +6,7 @@ import ModalForm from "./_modal-form.vue";
const oruga = useOruga();
function imageModal(): void {
+ // here we use a render function to create an dynamic inline component (https://vuejs.org/guide/extras/render-function)
const vnode = h("p", { style: { "text-align": "center" } }, [
h("img", {
src: "https://avatars2.githubusercontent.com/u/66300512?s=200&v=4",
@@ -13,7 +14,7 @@ function imageModal(): void {
]);
oruga.modal.open({
- content: [vnode],
+ component: vnode,
});
}
diff --git a/packages/oruga/src/components/modal/useModalProgrammatic.ts b/packages/oruga/src/components/modal/useModalProgrammatic.ts
index 9052b65ac..5092ce9c5 100644
--- a/packages/oruga/src/components/modal/useModalProgrammatic.ts
+++ b/packages/oruga/src/components/modal/useModalProgrammatic.ts
@@ -1,8 +1,4 @@
-import {
- type Component,
- type ComponentInternalInstance,
- type VNodeTypes,
-} from "vue";
+import { type Component, type ComponentInternalInstance } from "vue";
import {
InstanceRegistry,
ComponentProgrammatic,
@@ -25,10 +21,9 @@ const instances = new InstanceRegistry();
/** useModalProgrammatic composable options */
export type ModalProgrammaticOptions = Readonly<
- Omit, "content">
-> & {
- content?: string | Array;
-} & ProgrammaticComponentOptions;
+ ModalProps
+> &
+ ProgrammaticComponentOptions;
const ModalProgrammatic = {
/**
@@ -44,30 +39,18 @@ const ModalProgrammatic = {
const _options: ModalProgrammaticOptions =
typeof options === "string" ? { content: options } : options;
- let slot;
- // render content as slot when is an array
- if (Array.isArray(_options.content)) {
- slot = _options.content;
- delete _options.content;
- }
-
const componentProps: ModalProps = {
active: true, // set the active default state to true
...(_options as ModalProps),
};
// create programmatic component
- return ComponentProgrammatic.open(
- Modal as VNodeTypes,
- {
- instances, // custom programmatic instance registry
- target, // target the component get rendered into
- props: componentProps, // component specific props
- onClose: _options.onClose, // on close event handler
- },
- // component default slot to render content
- slot,
- );
+ return ComponentProgrammatic.open(Modal, {
+ instances, // custom programmatic instance registry
+ target, // target the component get rendered into
+ props: componentProps, // component specific props
+ onClose: _options.onClose, // on close event handler
+ });
},
/** Close the last registred instance in the modal programmatic instance registry. */
close(...args: unknown[]): void {
diff --git a/packages/oruga/src/components/notification/NotificationNotice.vue b/packages/oruga/src/components/notification/NotificationNotice.vue
index bcdbf2cbd..a26d978db 100644
--- a/packages/oruga/src/components/notification/NotificationNotice.vue
+++ b/packages/oruga/src/components/notification/NotificationNotice.vue
@@ -75,6 +75,7 @@ onBeforeMount(() => {
if (parentTop.value && parentBottom.value) return;
+ // create notices top container if not alread there
if (!parentTop.value) {
parentTop.value = document.createElement("div");
parentTop.value.className = `${rootClasses.join(
@@ -82,6 +83,7 @@ onBeforeMount(() => {
)} ${topClasses.join(" ")}`;
}
+ // create notices bottom container if not alread there
if (!parentBottom.value) {
parentBottom.value = document.createElement("div");
parentBottom.value.className = `${rootClasses.join(
@@ -89,6 +91,7 @@ onBeforeMount(() => {
)} ${bottomClasses.join(" ")}`;
}
+ // append notices top and bottom container to given container
props.container.appendChild(parentTop.value);
props.container.appendChild(parentBottom.value);
@@ -133,6 +136,7 @@ const shouldQueue = computed(() =>
: false,
);
+/** move the rendered component template into the correct parent container */
function showNotice(): void {
if (!correctParent.value) return;
diff --git a/packages/oruga/src/components/notification/props.ts b/packages/oruga/src/components/notification/props.ts
index 8486ccdef..8067e6b7a 100644
--- a/packages/oruga/src/components/notification/props.ts
+++ b/packages/oruga/src/components/notification/props.ts
@@ -5,8 +5,8 @@ import type { ComponentProps } from "vue-component-type-helpers";
export type NotificationProps = {
/** Override existing theme classes completely */
override?: boolean;
- /** Message text (can contain HTML), unnecessary when default slot is used */
- message?: string | string[];
+ /** Message text, unnecessary when default slot is used */
+ message?: string;
/** Whether modal is active or not, use v-model:active to make it two-way binding */
active?: boolean;
/**
diff --git a/packages/oruga/src/components/notification/useNotificationProgrammatic.ts b/packages/oruga/src/components/notification/useNotificationProgrammatic.ts
index 5ba4753ad..18ef79d6c 100644
--- a/packages/oruga/src/components/notification/useNotificationProgrammatic.ts
+++ b/packages/oruga/src/components/notification/useNotificationProgrammatic.ts
@@ -22,13 +22,9 @@ const instances = new InstanceRegistry();
/** useNotificationProgrammatic composable options */
export type NotificationProgrammaticOptions = Readonly<
- Omit<
- NotificationNoticeProps & NotificationProps,
- "message" | "container"
- >
-> & {
- message?: string | Array;
-} & ProgrammaticComponentOptions;
+ Omit & NotificationProps, "container">
+> &
+ ProgrammaticComponentOptions;
const NotificationProgrammatic = {
/** Returns the number of registered active instances. */
@@ -46,13 +42,6 @@ const NotificationProgrammatic = {
const _options: NotificationProgrammaticOptions =
typeof options === "string" ? { message: options } : options;
- let slot;
- // render message as slot when is an array
- if (Array.isArray(_options.message)) {
- slot = _options.message;
- delete _options.message;
- }
-
const componentProps: NotificationNoticeProps = {
position: getOption("notification.position", "top-right"),
container: document.body,
@@ -60,17 +49,12 @@ const NotificationProgrammatic = {
};
// create programmatic component
- return ComponentProgrammatic.open(
- NotificationNotice,
- {
- instances, // custom programmatic instance registry
- target, // target the component get rendered into
- props: componentProps, // component specific props
- onClose: _options.onClose, // on close event handler
- },
- // component default slot render
- slot,
- );
+ return ComponentProgrammatic.open(NotificationNotice, {
+ instances, // custom programmatic instance registry
+ target, // target the component get rendered into
+ props: componentProps, // component specific props
+ onClose: _options.onClose, // on close event handler
+ });
},
/** Close the last registred instance in the notification programmatic instance registry. */
close(...args: unknown[]): void {
diff --git a/packages/oruga/src/components/programmatic/ProgrammaticComponent.ts b/packages/oruga/src/components/programmatic/ProgrammaticComponent.ts
index db48a8109..bbabea1fa 100644
--- a/packages/oruga/src/components/programmatic/ProgrammaticComponent.ts
+++ b/packages/oruga/src/components/programmatic/ProgrammaticComponent.ts
@@ -103,6 +103,7 @@ export const ProgrammaticComponent = defineComponent<
);
},
{
+ name: "ProgrammaticApp",
// manual runtime props declaration is currently still needed.
props: ["component", "props", "instances"],
// manual runtime emits declaration
diff --git a/packages/oruga/src/components/programmatic/tests/useProgrammatic.test.ts b/packages/oruga/src/components/programmatic/tests/useProgrammatic.test.ts
index 85d546cd0..c144f8d4e 100644
--- a/packages/oruga/src/components/programmatic/tests/useProgrammatic.test.ts
+++ b/packages/oruga/src/components/programmatic/tests/useProgrammatic.test.ts
@@ -152,19 +152,22 @@ describe("useProgrammatic tests", () => {
});
test("test closeAll is working correctly", async () => {
+ const root = document.createElement("div");
+
// open elements
- ComponentProgrammatic.open("div");
- ComponentProgrammatic.open("div");
+ ComponentProgrammatic.open("div", { target: root });
+ ComponentProgrammatic.open("div", { target: root });
- let bodyElements = document.body.querySelectorAll("*");
- expect(bodyElements).toHaveLength(2);
+ let apps = root.querySelectorAll("#programmatic-app");
+ expect(apps).toHaveLength(2);
// close all elements
ComponentProgrammatic.closeAll();
vi.runAllTimers();
- bodyElements = document.body.querySelectorAll("*");
- expect(bodyElements).toHaveLength(0);
+ // check elements are removed
+ apps = root.querySelectorAll("#programmatic-app");
+ expect(apps).toHaveLength(0);
});
test("test close last is working correctly", async () => {
@@ -172,34 +175,36 @@ describe("useProgrammatic tests", () => {
ComponentProgrammatic.open("div");
ComponentProgrammatic.open("div");
- let bodyElements = document.body.querySelectorAll("*");
+ let bodyElements = document.body.querySelectorAll("#programmatic-app");
expect(bodyElements).toHaveLength(2);
// close last element
ComponentProgrammatic.close();
vi.runAllTimers();
- bodyElements = document.body.querySelectorAll("*");
+ bodyElements = document.body.querySelectorAll("#programmatic-app");
expect(bodyElements).toHaveLength(1);
// close last element
ComponentProgrammatic.close();
vi.runAllTimers();
- bodyElements = document.body.querySelectorAll("*");
+ bodyElements = document.body.querySelectorAll("#programmatic-app");
expect(bodyElements).toHaveLength(0);
});
test("test render slot correctly", async () => {
- const component = createVNode({
- template: ``,
- });
-
// create inner slot element
const slot = h("p", { "data-oruga": "inner-slot" }, "HELP");
+ const component = createVNode(
+ { template: `` },
+ null,
+ () => slot,
+ );
+
// open elements
- const { close } = ComponentProgrammatic.open(component, {}, slot);
+ const { close } = ComponentProgrammatic.open(component);
// check element exist
const button = document.body.querySelector("button");
diff --git a/packages/oruga/src/components/programmatic/useProgrammatic.ts b/packages/oruga/src/components/programmatic/useProgrammatic.ts
index 647119c9f..683243ab3 100644
--- a/packages/oruga/src/components/programmatic/useProgrammatic.ts
+++ b/packages/oruga/src/components/programmatic/useProgrammatic.ts
@@ -1,9 +1,8 @@
import {
- createVNode,
- render,
+ createApp,
+ type App,
type ComponentInternalInstance,
type EmitsToProps,
- type VNode,
type VNodeTypes,
} from "vue";
@@ -30,10 +29,15 @@ const instances = new InstanceRegistry();
/** useProgrammatic composable `open` function options */
export type ProgrammaticOptions = {
/**
- * Specify a target the component get rendered into
+ * Specify a target the component get rendered into.
* @default `document.body`
*/
target?: string | HTMLElement | null;
+ /**
+ * Specify the template `id` for the programmatic container element.
+ * @default `programmatic-app`
+ */
+ appId?: string;
} & Omit, "component"> & // component props
EmitsToProps>; // component emit props
@@ -54,12 +58,10 @@ export const ComponentProgrammatic = {
* Create a new programmatic component instance.
* @param component component to render
* @param options render options
- * @param slot default slot content - see {@link https://vuejs.org/api/render-function.html#render-function-apis |Vue render function}
*/
open(
component: C,
options?: ProgrammaticOptions,
- slot?: unknown,
): ProgrammaticExpose {
options = { instances, ...options };
@@ -72,39 +74,45 @@ export const ComponentProgrammatic = {
? options.target
: document.body;
- // cache container
- let container: HTMLDivElement | null = document.createElement("div");
+ // create app container
+ let container: HTMLDivElement | undefined =
+ document.createElement("div");
+ container.id = options.appId || "programmatic-app";
+
+ // place the app container into the target element
+ target.appendChild(container);
- // clear vnode
+ // clear instance handler
function onDestroy(): void {
- // clear the container and all connected child node by rendering null into it
- if (container) render(null, container);
- container = null; // reset the variable
- vnode = null; // reset the vnode
+ // destroy app/component
+ if (app) {
+ app.unmount();
+ app = undefined;
+ }
+ // clear container
+ if (container) {
+ target.removeChild(container);
+ container = undefined;
+ }
}
- // create dynamic component
- let vnode: VNode | null = createVNode(
- ProgrammaticComponent,
- {
- instances: options.instances, // programmatic registry instance - can be overriden by given in options
- component, // the component which should be rendered
- props: { ...options.props, container: target }, // component props including the target as `container`
- onClose: options.onClose, // custom onClose handler
- onDestroy, // node destory cleanup handler
- } as ProgrammaticComponentProps,
- slot ? (): unknown => slot : null, // default slot render function
- );
- if (VueInstance?._context) vnode.appContext = VueInstance._context; // set app context
-
- // render a new vue instance into the cache container
- render(vnode, container);
-
- // place rendered elements into target element
- target.append(...container.children);
-
- // return exposed functionalities
- return vnode.component?.exposed as ProgrammaticExpose;
+ // create a new vue app instance with the ProgrammaticComponent as root
+ let app: App | undefined = createApp(ProgrammaticComponent, {
+ instances: options.instances, // programmatic registry instance - can be overriden by given in options
+ component, // the component which should be rendered
+ props: { ...options.props, container: target }, // component props including the target as `container`
+ onClose: options.onClose, // custom onClose handler
+ onDestroy, // node destory cleanup handler
+ });
+
+ // share the current context to the new app instance if running inside a nother app
+ if (VueInstance) app._context = VueInstance._context;
+
+ // render the new vue instance into the container
+ app.mount(container);
+
+ // return exposed programmatic functionalities
+ return app?._instance?.exposed as ProgrammaticExpose;
},
/** close the last registred instance in the global programmatic instance registry */
close(...args: unknown[]): void {
diff --git a/packages/oruga/src/components/sidebar/useSidebarProgrammatic.ts b/packages/oruga/src/components/sidebar/useSidebarProgrammatic.ts
index 0bf14ae76..c7159c521 100644
--- a/packages/oruga/src/components/sidebar/useSidebarProgrammatic.ts
+++ b/packages/oruga/src/components/sidebar/useSidebarProgrammatic.ts
@@ -1,8 +1,4 @@
-import {
- type Component,
- type ComponentInternalInstance,
- type VNodeTypes,
-} from "vue";
+import { type Component, type ComponentInternalInstance } from "vue";
import {
InstanceRegistry,
ComponentProgrammatic,
@@ -48,7 +44,7 @@ const SidebarProgrammatic = {
};
// create programmatic component
- return ComponentProgrammatic.open(Sidebar as VNodeTypes, {
+ return ComponentProgrammatic.open(Sidebar, {
instances, // custom programmatic instance registry
target, // target the component get rendered into
props: componentProps, // component specific props