-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathrenderer_target.vue
More file actions
151 lines (132 loc) · 3.15 KB
/
renderer_target.vue
File metadata and controls
151 lines (132 loc) · 3.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<template>
<div
ref="wrapper"
/>
</template>
<script setup>
import { capitalize, computed, h, nextTick, onMounted, onUpdated, ref, render, useSlots } from 'vue';
import { DtNotice } from '@dialpad/dialtone-vue';
const ERROR_MESSAGE = 'Invalid combination';
const props = defineProps({
/**
* Component to render.
*/
component: {
type: Object,
required: true,
},
/**
* Members to bind to the target component.
*/
bindings: {
type: undefined,
required: true,
},
/**
* Events to bind to the target component.
*/
events: {
type: undefined,
required: true,
},
/**
* Set of member names that are currently disabled.
*/
disabledMembers: {
type: Set,
default: () => new Set(),
},
});
const emit = defineEmits([
'event',
]);
const slots = useSlots();
/**
* Map object containing events and their respective handlers.
*
* @returns {ComputedRef<object>} Event map.
*/
const events = computed(() => {
if (!props.events) { return {}; }
return Object.fromEntries(
props.events.map(({ name }) => {
return [
`on${capitalize(name)}`,
e => emit('event', name, e),
];
}),
);
});
let currentContainer = null;
onMounted(() => {
currentContainer = freshContainer();
renderTarget();
nextTick(renderTarget);
});
onUpdated(renderTarget);
const wrapper = ref();
/**
* Properly unmounts any existing component, clears the wrapper,
* and creates a fresh container element for rendering.
*
* @returns {HTMLDivElement} Instantiated container for rendering.
*/
function freshContainer () {
if (wrapper.value.firstChild) {
render(null, wrapper.value.firstChild);
}
wrapper.value.replaceChildren();
return wrapper.value.appendChild(document.createElement('div'));
}
/**
* Need to render manually to catch DOM exception errors.
*
* Renders the target component into the current container.
* Reuses the existing container so Vue patches the component
* instance (preserving DOM and Floating UI state) rather than
* unmounting and remounting on every prop change.
*/
function renderTarget () {
if (!currentContainer) {
currentContainer = freshContainer();
}
const filteredBindings = Object.fromEntries(
Object.entries(props.bindings).filter(([name]) => !props.disabledMembers.has(name)),
);
const slotKey = Object.keys(slots).sort().join(',');
try {
render(h(props.component, {
...filteredBindings,
...events.value,
key: slotKey,
}, slots), currentContainer);
} catch (e) {
console.warn('Rendering warning: \n', e);
currentContainer = freshContainer();
renderError(e, currentContainer);
}
}
/**
* Renders the error 'notice' component.
*
* @param exception - The exception.
* @param container - The container to render in.
*/
function renderError (exception, container) {
render(h(DtNotice, {
kind: 'critical',
hideClose: true,
title: ERROR_MESSAGE,
}, {
default: () => exception.toString(),
}), container);
}
</script>
<script>
/**
* The renderer is responsible for displaying the target component in its current state.
*/
export default {
name: 'DtcRendererTarget',
};
</script>