Search code examples
svelte

How do you implement file upload on a Svelte app that works on all resolutions?


So the problem I'm having is fairly niche, so I'm not sure if this has happened to anyone else. In a Svelte app I'm working on, I tried to implement simple file uploading. Here is my folder structure:

  • App.Svelte
    • Home
      • Home.Svelte
      • ImageUpload
        • ImageUpload.Svelte
      • DesktopView
        • DesktopView.Svelte
      • MobileView
        • MobileView.Svelte

Here's the MobileView.Svelte file

<script lang="ts">
    import ImageUpload from "../ImageUpload/ImageUpload.svelte"
</script>

<main>
    <h1>Mobile View</h1>
    <ImageUpload />
</main>

<style lang="scss">
    main{
        @media screen and (min-width: 992px) {
            display: none;
        }
    }
</style>

Here's DesktopView.Svelte

<script lang="ts">
    import ImageUpload from "../ImageUpload/ImageUpload.svelte"
</script>

<main>
    <h1>Desktop View</h1>
    <ImageUpload />
</main>

<style lang="scss">
    main{
        display: none;

        @media screen and (min-width: 992px) {
            display: unset;
        }
    }
</style>

Here is the ImageUpload.Svelte file.

<script lang="ts">
    import { onMount } from "svelte";
    import { Image } from "svelte-bootstrap-icons";
    
    let imageURL:     any 
    let showImageUrl: boolean = false

    const onFileSelected = (e: any)=>{
        const file = e.target.files[0];
        const reader = new FileReader();

        reader.onload = () => {
            imageURL = reader.result;
        };

        reader.readAsDataURL(file);
    }

    onMount(() => {
        imageURL = null
    })
</script>

<main>
    <div class="icon-wrapper image-select">
        <label for="file-input">
            <Image width={25} height={25} />
        </label>
        <input id="file-input" type="file" accept="image/x-png,image/gif,image/jpeg"  on:change={(e)=>onFileSelected(e)}  />
    </div>        
    <div class="image-wrapper">
        <img src={imageURL} alt="" width="200px" height="auto">
    </div>
    <button on:click={() => showImageUrl = !showImageUrl}>Send Picture</button>
    {#if imageURL}
        <h1>image</h1>
    {:else}
        <h1>no image</h1>
    {/if}

    {#if showImageUrl}
        <h2>{imageURL}</h2>
    {/if}
    <span class="close" on:click={() => imageURL = null} role="button" tabindex="0" on:keyup={null}>
        &times;
    </span>
</main>

<style lang="scss">
    input{
        display: none;
    }
</style>

Finally, here's the Home.Svelte file.

<script lang="ts">
    import DesktopView from "./DesktopView/DesktopView.svelte";
    import MobileView from "./MobileView/MobileView.svelte";
</script>

<main>
    <MobileView />
    <DesktopView />
</main>

For whatever reason, the ability to upload images works as it should for whichever view component is on top, but not for the one at the bottom. By "not working", I mean that when the image is chosen, the imageURL variable is left null even after filling it with image metadata. In the Home.Svelte file above, When the MobileView component is on top, image uploading works there, but not for the DesktopView component, and of course vice versa for when the DesktopView component is on top.

Is this a quirk of the framework, or is there something fundamental that I'm missing? I not sure why the behavior changes when the order of the two view components are swapped, considering that they're both using the same ImageUpload component.


Solution

  • Is this a quirk of the framework,

    No it is not.

    or is there something fundamental that I'm missing?

    Yes

    I not sure why the behavior changes when the order of the two view components are swapped, considering that they're both using the same ImageUpload component.

    This is because you are using ids in the ImageUpload component. This is the way the DOM works. You aren't supposed to have multiple elements with the same id, but if you decide to do so then whatever element comes first in the DOM with that id will be the one that selectors, the for attribute, and other such things apply to.

    To fix this you should avoid using fixed ids in your ImageUpload component I say fixed because you could dynamically generate the id attribute based on say the index of the component something like:

    <script context="module">
    let counter = 0;
    </script>
    
    <script>
    const id = counter++;
    </script>
    
    <div {id} />
    

    if you want to use ids. The alternative would be to nest your form control inside of your label which avoids the need for the id attribute in this case as in:

    <label>
        The label
        <input />
    </label>
    

    As a side note, the pattern you are using where you have like separate components for desktop and mobile that render the same things with different styling is not a good idea. (We are straying slightly into opinion territory here), but when making responsive sites you should support all screen sizes not just 2 specific ones and you should generally avoid rendering the same thing twice and hiding it based on media queries for many different reasons if you possibly can. Some reasons to avoid doing this:

    • You are sending duplicate HTML to the client (bigger payloads)
    • You are sending invalid semantic HTML without the CSS. Having 2 main elements is invalid. (Don't know if it's valid with one of them hidden via css, but even if it is then say the CSS fails to load then you have an invalid document)