Render function rerenders all children when one gets added
Vue version
3
Steps to reproduce
I will provide a reproduction tomorrow, the project is sadly a little bloated and stackblitz can't find my package.json file at the moment.
I am trying to build a generic like rendering component which gets an JSON Array of "Webnodes" as input and renders them.
This is my Component:
<script lang="ts">
import { h} from 'vue';
import { HTMLWebNode } from '../classes/HTMLWebNode';
import { defineAsyncComponent } from 'vue';
import { Teleport } from 'vue';
import { useWebNodeStore } from 'src/stores/useWebNodeStore'
export default {
name: 'BaseRenderer',
props: {
defaulttag: {
type: String,
required: false,
default: 'tag:div'
},
contextid: {
type: Number,
required: false
},
webnode:{
type: Object,
reuqired: false
},
webnodes:{
type: Array,
required: false
},
rootprops:{
type: Object,
required: false
}
},
setup(props) {
const store = useWebNodeStore()
function HTMLorProps(node: HTMLWebNode) {
if(node.html != null && node.html != undefined) {
return [null, '12313123123123'];
}else if(node.tag?.startsWith('tag')){
return node.htmlattributes
}else if(node.tag == 'component:Teleport'){
return {to: node.htmlattributes.to}
}
else if(node.tag?.startsWith('component')){
return {element: node, contextid: props?.contextid};
}
}
function resolveComponent(node: HTMLWebNode){
if(node.tag == 'component:Teleport'){
return Teleport;
}
if(node.tag?.startsWith('component:')){
const component = defineAsyncComponent(() => import('/src/components/' + node.tag.replace('component:', '') + '.vue'))
return component;
}
}
function setTag(node: HTMLWebNode){
if(node.tag == undefined){
return props.defaulttag.replace('tag:', '');
}
else if(node.tag?.startsWith('tag:')){
const pos = node.tag?.lastIndexOf(':')
const index = node.tag?.indexOf(':', pos) +1
return node.tag.slice(index)
}else if(node.tag?.startsWith('component:')){
return resolveComponent(node)
}
}
function getChildren(node: HTMLWebNode){
if(props.webnodes){
return props.webnodes.filter((n) => node.children?.includes(n.id))
}else{
return store.getChildren(props.contextid, node)
}
}
function render(node: HTMLWebNode){
return h(setTag(node), HTMLorProps(node), {default: () => getChildren(node)?.map((child) => {
return render(child);
}),
});
}
function renderContextRoot(){
const contextrootelement = store.byKey(props.contextid, 'type', 'environment')
if(contextrootelement != undefined){
return render(contextrootelement)
}
}
function renderRoot(){
if(props.webnode){
return render(props.webnode)
}else if(props.webnodes){
return h(props.defaulttag.replace('tag:', ''), props.rootprops, {default: () => props.webnodes?.map((node) => {
return render(node);
})
})
}
else{
return renderContextRoot();
}
}
return renderRoot;
}
}
</script>
This is the Pinia store where the Arrays are saved. The render() function in the component uses the getter function "getChildren".
`import { defineStore } from 'pinia';
import { HTMLWebNode } from 'app/utils/interfaces/interfaces';
import { createUniqueClientIdInContext } from 'app/src/composables/createUniqueClientIdInContext';
import { WebNodeContext } from 'app/utils/interfaces/interfaces';
import { WebNode } from 'app/utils/classes/WebNode';
export const useWebNodeStore = defineStore('webnodes',{
state: () => ({
webnodecontexts: Array<WebNodeContext>() ,
registerednodeids: Array<number>()
}),
getters: {
getContextComponentRefs: (state) => {
return (contextid: number) => {
return state.webnodecontexts.find(c => c.contextid == contextid)?.componentrefs
}
},
getContextWebNodes: (state) => {
return (contextid?: number, name?: string) => {
if(name){
return state.webnodecontexts.find(c => c.optionaluniquename == name)?.webnodes
}else{
return state.webnodecontexts.find(c => c.contextid == contextid)?.webnodes
}
}
},
getContextWebNodesbyType(){
return (contextid?: number, contextname?:string, types?: Array<string>) => {
let elements: Array<HTMLWebNode> = []
const context: WebNodeContext = this.getContext(contextid, contextname)
elements = this.getContextWebNodes(context.contextid)?.filter(n => types.includes(n.type))
elements.forEach(e => {
if(e.children){
e.children.forEach(c => {
const child = this.byId(context.contextid, c)
elements.push(child)
})
}
})
return [elements, context.contextid];
}
},
getContext(){
return (contextid?: number, name?:string) => {
if(contextid != undefined){
return this.webnodecontexts.find(c => c.contextid == contextid)
}else if(name){
return this.webnodecontexts.find(c => c.optionaluniquename == name)
}
}
},
byId(): HTMLWebNode | undefined{
return (contextid: number, id: number) => {
return this.webnodecontexts.find(c => c.contextid == contextid).webnodes.find(n => n.id == id)
}
},
byKey(): HTMLWebNode | undefined{
return (contextid: number, key: string, value: any) => {
return this.webnodecontexts.find(c => c.contextid == contextid).webnodes.find(n => n[key] == value)
}
},
getChildren(){
return (contextid: number, node: WebNode) => {
return this.webnodecontexts.find(c => c.contextid == contextid).webnodes.filter(n => node.children?.includes(n.id))
}
}
},
actions: {
addWebNodeContext(name?: string): number{
const newid: number = createUniqueClientIdInContext(this.webnodecontexts.map(c => { return c.contextid }))
const newcontext: WebNodeContext= { optionaluniquename: name, contextid: newid, webnodes: Array<HTMLWebNode>(), componentrefs: [], nodetree: {nodeid: undefined, children: []}};
this.webnodecontexts.push(newcontext)
return newid;
},
deleteWebNodeContext(contextid: number ): string | number{
const index = this.webnodecontexts.findIndex(c => c.contextid == contextid)
let message: string | number = this.webnodecontexts[index].contextid
try{
this.webnodecontexts.splice(index, 1)
} catch(e: any){
message = e
}
return message
},
addWebNodetoContext(contextid: number, webnode: HTMLWebNode, parentid?: number): WebNode{
const context = this.webnodecontexts.find(c => c.contextid == contextid)
context.webnodes.push(webnode)
if(parentid != undefined && typeof parentid == 'number'){
const parent = this.byId(contextid, parentid)
if(!parent.children.includes(webnode.id)){
parent.children.push(webnode.id)
}
}else if(context.webnodes.find(n => n?.type == 'environment') != undefined && webnode.type != 'environment'){
const parent = this.byKey(contextid, 'type', 'environment')
if(!parent.children.includes(webnode.id)){
parent.children.push(webnode.id)
}
}
if(webnode.htmlattributes?.key === undefined){
if(webnode.htmlattributes === undefined){
webnode.htmlattributes = {}
}
webnode.htmlattributes.key = webnode.id
}
return webnode;
},
changeNodeContext(contextid: number, elementid: number, newcontextid: number, newparentid?: number) {
const node = this.byId(contextid, elementid)
this.addWebNodetoContext(newcontextid, node, newparentid)
const add = this.addWebNodetoContext;
const delet = this.deleteNode;
const find = this.byId
function iterateChildren(webnode){
if(webnode.children?.length >= 0){
for(const child of webnode.children){
const childnode = find(contextid, child)
add(newcontextid, childnode, webnode.id)
delet(contextid, child)
if(childnode.children?.length >= 0){
iterateChildren(childnode)
}
}
}
}
iterateChildren(node)
this.deleteNode(contextid, elementid)
},
createIdentifier(){
const id: number | string = Math.max(...this.registerednodeids);
if(id === -Infinity){
this.registerednodeids.push(0)
return 0
}
else if(id >= 0){
this.registerednodeids.push(id+1)
return id +1
}
},
deleteNode(contextid: number, elementid: number) {
const i: number = this.getContextWebNodes(contextid)?.findIndex(n => n.id == elementid)
this.webnodecontexts.find(c => c.contextid == contextid)?.webnodes.splice(i, 1)
}
}
})
Example for an object of the json array:
{
"name": "Loop Task",
"type": "bpmn:activity:task:looptask",
"tag": "component:Modelling/Shapes/ModellingRectangleElement", // could also be "tag:div"
"htmlattributes": {
"xmlns": "http://www.w3.org/2000/svg",
"width": 60,
"height": 40,
"class": "base-modelling-element"
},
"children": [
// array of objects with the same structure
],
"specifications": {
"aspectratio": "3:4"
}
},
Thank you all :)d
What is expected?
When the object get updated through the store, for example setting the "htmlattributes.widht" = 50, only this elements gets rerendern. When a new objects is added, only the new object gets rendered.
What is actually happening?
Updating some of the objects properties works fine. When I change the width or something else, only this element gets rerenderd. But if I add a new elements, all elements get rerenderd. If the element is a vue component, the setup function executes again.
System Info
No response
Any additional comments?
No response
Came out that the problem was the slot in the "ModellingElement" component. I modified my "BaseRenderer" Component so that I'm able to work around the slot in the ModellingElement component.
I changed this:
<template>
<Teleport :to="'#'+ props.element.properties.envcontextid">
<slot></slot>
</Teleport>
</template>
<script setup lang="ts">
import BaseRenderer from 'app/utils/Renderer/BaseRenderer.vue';
import { useWebNodeStore } from 'src/stores/useWebNodeStore';
const props = defineProps({
element: {
type: Object,
required: true
},
contextid: {
type: Number,
required: true
}
});
</script>`
to this:
`<template>
<Teleport :to="'#'+ props.element.properties.envcontextid">
<BaseRenderer :parentid="props.element.id" :contextid="props.contextid"></BaseRenderer>
</Teleport>
</template>
<script setup lang="ts">
import BaseRenderer from 'app/utils/Renderer/BaseRenderer.vue';
import { useWebNodeStore } from 'src/stores/useWebNodeStore';
const props = defineProps({
element: {
type: Object,
required: true
},
contextid: {
type: Number,
required: true
}
});
</script>`
```
Related issue: #5579