Subscribe on changes!

Render function rerenders all children when one gets added

avatar
Apr 17th 2023

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

avatar
Apr 18th 2023

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