Search code examples
javascriptobjectsveltesveltekitreactive

How to listen to specific changes in an object key


I'm writing a svelte code in which I have an object that contains attributes of a HTML element. I want to listen to changes in the object to be able to modify the element in the DOM. However, I noticed that I'm going to have to write line by line a listener for every attribute of the object. I wanted to know if this is proper syntax or if there is a better way to do it.

Edit:

The node element is defined by a user double-clicking an element on the display. The app initiates with the Display element by default, but changes when the user double clicks. The object appearance is modified through UI components that the user has in a side panel.

Finish edit:

Here is an abstraction of my code. This would go inside a script tag. Also note that the keys of the object would be binded to elements in the DOM.

   let node:HTMLElement = document.getElementById("Display")
   let appearance:ElementAppearance ={
      background_color = "#ffffff",
      text_color = "#000000",
      width: "w-full"
      //....so on 
   }

   $:appearance.background_color,handle_background_color(node,appearance.background_color)
   $:appearance.text_color,handle_text_color(node,appearance.text_color)
   $:appearance.width,handle_width(node,appearance.width)
   //..... so on

therefore I would like to know if there is a better sintaxis for this? maybe something similar to this

   $:appearance:{
       $background_color:handle_background_color(node,$background_color)
       //.... so on
   }

Edit:

Here is some additional context to this problem. The elements are generated in a toolbar component, in which the options are rendered into view. Here is an abstraction of that component.

<script lang="ts">
       elements:string[]=["div","h1","h2",....so on]
       
<script>
<div class="absolute border-2 w-1/4 min-h-52 bg-gray-50 rounded shadow-md">
    {#each elements as element}
        <div class="p-2 border flex justify-center">
            <svelte:element use:handle_drag={element} class="{define_default_styles(element)}" this={element} role="button">
                {#if element !== "input"}
                {define_default_content(element)}
                {/if}
            </svelte:element>
        </div>
    {/each}
</div>

From here, we only need to look into the handle drag action. Which all it does is pass the type of element to be added to the canvas.

export function handle_drag(node:HTMLElement,data:string){
    node.draggable=true
    node.style.cursor = "grab"

    function handle_dragstart(e:DragEvent){
        if(e.dataTransfer){
            e.dataTransfer.setData("text/plain",data)
        }
    }

    node.addEventListener("dragstart", handle_dragstart)
}

Then added into the canvas which looks like this

    <script lang="ts">
    import { drop_zone } from "$lib/helpers/canvas/canva_functionality";

    let canvas:HTMLElement
   
</script>


<div id="canvas" class="min-h-[95vh]" bind:this={canvas} use:drop_zone={{element:canvas}}>
</div>

From this, we need to look at drop_zone action, which is quite extensive. But basically what it does, it gives the elements all sort of special characteristics that allow the user to modify them more freely inside the canvas. as well as define which sort of elements are containers and which aren't.

Inside that method, a double click event listener is added in which a store is updated. That store is being listed to by the parent containing the original abstraction (the one that I originally posted). and passed as a prop to the component containing that logic.

The reason it's listening to it in the parent is because the selected node could have different categories of visual editing, which will be separated into different components.

Finish edit:


Solution

  • This is again more complicated than it needs to be.

    You can create a data model for all the added elements and render them directly in the markup via {#each} and <svelte:element>.

    Editing then becomes as simple as:

    • Setting a variable to the selected element model
    • Adding inputs in markup that edit properties of the selected model
    • Invalidating the list of elements so the UI updates.

    E.g.

    let elements = [];
    let selected = undefined;
    
    // Information for property editing,
    // could also be associated with specific tags
    const properties = {
        background: { label: 'Background', type: 'color' },
        color: { label: 'Color', type: 'color' },
    };
    
    // Adds a new element
    function add(tag) {
        elements = [...elements, {
            tag,
            properties: [
                { id: 'background', value: 'none' },
                { id: 'color', value: 'inherit' },
            ],
            content: { kind: 'text', value: `${tag} element` },
        }];
    }
    
    const style = element =>
        element.properties
            .map(p => `${p.id}: ${p.value}`)
            .join('; ');
    
    // does invalidation via dummy assignment
    const update = () => elements = elements;
    

    Rendering of elements:

    <div class="canvas">
        {#each elements as element (element)}
            <svelte:element this={element.tag} style={style(element)}
                    on:dblclick={() => selected = element} role="none">
                {#if element.content.kind == 'text'}
                    {element.content.value}
                {/if}
            </svelte:element>
        {/each}
    </div>
    

    This could be extended with other attributes, content types and recursion.

    This is currently not keyboard accessible and should be made accessible by adding focus navigation and selection via e.g. enter or selecting directly on focus.

    For editing, new inputs can be generated if selected has been set.
    For the limited content and property model given above it could look like this:

    {#if selected}
        <div class="properties" on:input={update}>
            {#if selected.content.kind == 'text'}
                <label>
                    <span class="label">Text</span>
                    <input type="text" bind:value={selected.content.value} />
                </label>
            {/if}
            {#each selected.properties as p}
                {@const meta = properties[p.id]}
                <label>
                    <span class="label">{meta.label}</span>
                    {#if meta.type == 'color'}
                        {#if p.value == 'none'|| p.value == 'inherit'}
                            <div>[{p.value}] <button on:click={() => p.value = '#000000'}>Set</button></div>
                        {:else}
                            <div><input type="color" bind:value={p.value} /> {p.value}</div>
                        {/if}
                    {/if}
                </label>
            {/each}
        </div>
    {/if}
    

    Full REPL example

    No direct DOM interaction is necessary to do this.