-
-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(programmatic): add programmatic feature (#944)
- Loading branch information
Showing
9 changed files
with
236 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
packages/oruga/src/components/notification/examples/programmatic.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<script setup lang="ts"> | ||
// @ts-expect-error Examples are loaded differently. | ||
import { useOruga } from "../../../../dist/oruga"; | ||
import NotificationForm from "./_notification-form.vue"; | ||
const oruga = useOruga(); | ||
async function component(): Promise<void> { | ||
const instance = oruga.programmatic.open({ | ||
component: NotificationForm, | ||
target: "#notification", | ||
}); | ||
// wait until the notification got closed | ||
const result = await instance.promise; | ||
oruga.notification.open({ | ||
duration: 5000, | ||
message: "Modal dialog returned " + JSON.stringify(result), | ||
variant: "info", | ||
position: "top", | ||
closable: true, | ||
}); | ||
} | ||
</script> | ||
|
||
<template> | ||
<section> | ||
<o-button | ||
label="Launch notification (component)" | ||
variant="warning" | ||
size="medium" | ||
@click="component" /> | ||
</section> | ||
</template> | ||
|
||
<style lang="scss"> | ||
.toast-notification { | ||
margin: 0.5em 0; | ||
text-align: center; | ||
box-shadow: | ||
0 1px 4px rgb(0 0 0 / 12%), | ||
0 0 6px rgb(0 0 0 / 4%); | ||
border-radius: 2em; | ||
padding: 0.75em 1.5em; | ||
pointer-events: auto; | ||
color: rgba(0, 0, 0, 0.7); | ||
background: #ffdd57; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 145 additions & 0 deletions
145
packages/oruga/src/components/programmatic/Programmatic.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { | ||
createVNode, | ||
defineComponent, | ||
render, | ||
getCurrentInstance, | ||
onMounted, | ||
onUnmounted, | ||
type Component, | ||
type ComponentInternalInstance, | ||
type VNode, | ||
} from "vue"; | ||
|
||
import InstanceRegistry from "@/utils/InstanceRegistry"; | ||
import { VueInstance } from "@/utils/plugins"; | ||
import { isElement, removeElement } from "@/utils/helpers"; | ||
import { isClient } from "@/utils/ssr"; | ||
|
||
import type { ProgrammaticExpose } from "@/types"; | ||
|
||
declare module "../../index" { | ||
interface OrugaProgrammatic { | ||
programmatic: typeof Programmatic; | ||
} | ||
} | ||
|
||
type ProgrammaticComponentProps = { | ||
/** | ||
* Component to be injected. | ||
* Terminate the component by emitting a 'close' event — emits('close') | ||
*/ | ||
component: string | Component; | ||
/** | ||
* Props to be binded to the injected component. | ||
* Both attributes and properties can be used in props. | ||
* Vue automatically picks the right way to assign it. | ||
* `class` and `style` have the same object / array value support like in templates. | ||
* Event listeners should be passed as onXxx. | ||
* @see https://vuejs.org/api/render-function.html#h | ||
*/ | ||
props?: Record<string, any>; | ||
/** Callback function to call on close event */ | ||
onClose?: (...args: unknown[]) => void; | ||
/** Destroy component on close event */ | ||
destroyable?: boolean; | ||
/** | ||
* This is used internally for programmatic usage | ||
* @ignore | ||
*/ | ||
instances: InstanceRegistry<ComponentInternalInstance>; | ||
}; | ||
|
||
const ProgrammaticComponent = defineComponent( | ||
(props: ProgrammaticComponentProps, { expose }) => { | ||
// getting a hold of the internal instance in setup() | ||
const vm = getCurrentInstance(); | ||
|
||
let resolve: (value?: unknown) => void = null; | ||
const promise = new Promise((p1) => { | ||
resolve = p1; | ||
}); | ||
|
||
onMounted(() => { | ||
props.instances.add(vm); | ||
}); | ||
|
||
onUnmounted(() => { | ||
props.instances.remove(vm); | ||
resolve.apply(null); | ||
}); | ||
|
||
function close(...args: unknown[]): void { | ||
// call handler if given | ||
if (typeof props.onClose === "function") | ||
props.onClose.apply(null, args); | ||
|
||
if (typeof props.destroyable === "undefined" || props.destroyable) { | ||
// use timeout for any animation to complete before destroying | ||
setTimeout(() => { | ||
const element = vm.vnode.el as Element; | ||
// remove the component from the container or the body tag | ||
if (element) { | ||
if (isClient) | ||
window.requestAnimationFrame(() => | ||
removeElement(element), | ||
); | ||
else removeElement(element); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
/** expose public functionalities for programmatic usage */ | ||
expose({ close, promise }); | ||
|
||
// render given component | ||
return (): VNode => | ||
createVNode(props.component, { ...props.props, onClose: close }); | ||
}, | ||
{ props: ["component", "props", "onClose", "destroyable", "instances"] }, | ||
); | ||
|
||
const instances = new InstanceRegistry<ComponentInternalInstance>(); | ||
|
||
export type ProgrammaticProps = { | ||
/** | ||
* Specify a target the component get rendered into. | ||
* @default `body` | ||
*/ | ||
target?: string | HTMLElement; | ||
} & Omit<ProgrammaticComponentProps, "instances">; | ||
|
||
const Programmatic = { | ||
open(props: ProgrammaticProps): ProgrammaticExpose { | ||
const target = | ||
typeof props.target === "string" | ||
? document.querySelector<HTMLElement>(props.target) | ||
: isElement(props.target) | ||
? (props.target as HTMLElement) | ||
: document.body; | ||
|
||
// cache container | ||
const container = document.createElement("div"); | ||
|
||
// create dynamic component | ||
const vnode = createVNode(ProgrammaticComponent, { | ||
...props, | ||
instances: instances, | ||
}); | ||
vnode.appContext = VueInstance._context; | ||
|
||
// render a new vue instance into the cache container | ||
render(vnode, container); | ||
|
||
// place rendered elements into target element | ||
target.append(...container.childNodes); | ||
|
||
// return exposed functionalities | ||
return vnode.component.exposed as ProgrammaticExpose; | ||
}, | ||
closeAll(...args: any[]): void { | ||
instances.walk((entry) => entry.exposed.close(...args)); | ||
}, | ||
}; | ||
|
||
export default Programmatic; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { App, Plugin } from "vue"; | ||
|
||
import Programmatic from "./Programmatic"; | ||
|
||
import { registerComponentProgrammatic } from "@/utils/plugins"; | ||
|
||
/** export programmatic specific types */ | ||
export type { ProgrammaticProps } from "./Programmatic"; | ||
|
||
/** export programmatic plugin */ | ||
export default { | ||
install(app: App) { | ||
registerComponentProgrammatic(app, "programmatic", Programmatic); | ||
}, | ||
} as Plugin; | ||
|
||
// no component export here |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters