Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render function rerenders all children when one gets added #8106

Closed
svenhue opened this issue Apr 17, 2023 · 1 comment
Closed

Render function rerenders all children when one gets added #8106

svenhue opened this issue Apr 17, 2023 · 1 comment

Comments

@svenhue
Copy link

svenhue commented Apr 17, 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

@svenhue
Copy link
Author

svenhue commented Apr 18, 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 

@svenhue svenhue closed this as completed Apr 18, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Sep 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant