Craig Howell
Craig Howell

Reputation: 1194

Svelte wrapping each slot

I am looking to create a Spacer element in svelte. I have accomplished what I have set out to which is allow a user to wrap elements in a <Space></Space> tag and pass props to space them evenly vertically/horizontally or even align the interior elements center/start/end/baseline. The issue I have is that I have to wrap each child element to <Space></Space> with a <SpaceItem></SpaceItem> to get the desired effect. Here is the example code:

App.svelte

<script>
    import {Space, SpaceItem} from './components/Space';
</script>

<Space>
    <SpaceItem>
        <Button primary>Primary</Button>
    </SpaceItem>
    <SpaceItem>
        <Button primary icon="add">Primary</Button>
    </SpaceItem>
    <SpaceItem>
        <Button danger>Danger</Button>
    </SpaceItem>
</Space>

Space.svelte

<script>
    import { SPACE } from './SpaceContext.svelte';
    import { setContext } from 'svelte';
    import { readable } from 'svelte/store';
    export let vertical = false;
    export let horizontal = true;
    export let alignCenter = false;
    export let alignStart = false;
    export let alignEnd = false;
    export let alignBaseline = false;
    export let large = false;
    export let middle = false;
    export let size = null;

    horizontal = vertical ? false : true;
    size = size ? size : large ? 24 : middle ? 16 : 8;

    const gap = readable(size);
    const direction = readable(horizontal ? 'horizontal' : 'vertical');

    setContext(SPACE, {
        gap,
        direction,
    });
</script>

<style>
    .space {
        display: -webkit-inline-box;
        display: -ms-inline-flexbox;
        display: inline-flex;
    }
    .vertical {
        -webkit-box-orient: vertical;
        -webkit-box-direction: normal;
        -ms-flex-direction: column;
        flex-direction: column;
    }
    .align-center {
        -webkit-box-align: center;
        -ms-flex-align: center;
        align-items: center;
    }
    .align-start {
        -webkit-box-align: start;
        -ms-flex-align: start;
        align-items: flex-start;
    }
    .align-end {
        -webkit-box-align: end;
        -ms-flex-align: end;
        align-items: flex-end;
    }
    .align-baseline {
        -webkit-box-align: baseline;
        -ms-flex-align: baseline;
        align-items: baseline;
    }
</style>

<div
    class="space"
    class:horizontal
    class:vertical
    class:size
    class:align-center={alignCenter}
    class:align-start={alignStart}
    class:align-end={alignEnd}
    class:align-baseline={alignBaseline}>
    <slot />
</div>

SpaceItem

<script>
    import { SPACE } from './SpaceContext.svelte';
    import { getContext } from 'svelte';
    const { gap, direction } = getContext(SPACE);

    let style;
    if ($direction === 'horizontal') {
        style = `margin-right: ${$gap}px;`;
    } else {
        style = `margin-bottom: ${$gap}px;`;
    }
</script>

<style>

</style>

<div class="item" {style}>
    <slot />
</div>

Is there a way to eliminate the <SpaceItem></SpaceItem> element and for <Space></Space> to wrap each element passed to its <slot /> with the <SpaceItem></SpaceItem>? I would like to only have to wrap all elements in one Space tag and not wrap each individual one in a SpaceItem tag.

Desired effect is for me to be able to write:

App.svelte

<script>
    import {Space, SpaceItem} from './components/Space';
</script>

<Space>
    <Button primary>Primary</Button>
    <Button primary icon="add">Primary</Button>
    <Button danger>Danger</Button>
</Space>

And still wrap each button with <SpaceItem></SpaceItem> within the Space.svelte file.

Upvotes: 3

Views: 2068

Answers (1)

Craig Howell
Craig Howell

Reputation: 1194

Here is a solution that I have worked out on my own but I am not sure if this will have any performance issues in a real world application so comments are welcome.

App.svelte

<script>
  import Space from './Space.svelte';
  import Button from './Button.svelte';
</script>

<Space>
  <Button primary>Primary</Button>
  <Button primary icon="add">Primary</Button>
  <Button danger>Danger</Button>
</Space>

<Space vertical large alignCenter>
  <Button primary icon="add">Primary</Button>
  <Button danger>Danger</Button>
</Space>

Space.svelte

<script>
  import { onMount } from 'svelte';
  import addWrapper from './utils/addWrapper';
  export let vertical = false;
  export let horizontal = true;
  export let alignCenter = false;
  export let alignStart = false;
  export let alignEnd = false;
  export let alignBaseline = false;
  export let large = false;
  export let middle = false;
  export let size = null;

  horizontal = vertical ? false : true;
  size = size ? size : large ? 24 : middle ? 16 : 8;

  const gap = size;
  const direction = horizontal ? 'horizontal' : 'vertical';
  let style;

  if (direction === 'horizontal') {
    style = `margin-right: ${gap}px;`;
  } else {
    style = `margin-bottom: ${gap}px;`;
  }
  let currentEl;

  onMount(async () => {
    addWrapper({
      node: currentEl,
      tag: 'div',
      style,
    });
  });
</script>

<style>
  .space {
    display: -webkit-inline-box;
    display: -ms-inline-flexbox;
    display: inline-flex;
  }
  .vertical {
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -ms-flex-direction: column;
    flex-direction: column;
  }
  .align-center {
    -webkit-box-align: center;
    -ms-flex-align: center;
    align-items: center;
  }
  .align-start {
    -webkit-box-align: start;
    -ms-flex-align: start;
    align-items: flex-start;
  }
  .align-end {
    -webkit-box-align: end;
    -ms-flex-align: end;
    align-items: flex-end;
  }
  .align-baseline {
    -webkit-box-align: baseline;
    -ms-flex-align: baseline;
    align-items: baseline;
  }
</style>

<div
  bind:this={currentEl}
  class="space"
  class:horizontal
  class:vertical
  class:align-center={alignCenter}
  class:align-start={alignStart}
  class:align-end={alignEnd}
  class:align-baseline={alignBaseline}
>
  <slot />
</div>

addWrapper.js

export default async function addWrapper({ node, tag, style }) {
  const children = node.childNodes;
  [].forEach.call(children, function (child) {
    if (child.nodeName !== "#text" && child.nodeName !== "#comment") {
      let wrapper = document.createElement(tag);
      wrapper.setAttribute("style", style);
      child.parentNode.insertBefore(wrapper, child);
      wrapper.appendChild(child);
    }
  });
}

Like is said this works and eliminates the need for creating individual wrappers for simple situations but more complex wrappers would require re-working. If anyone has a better suggestion that is simpler and more performant I am all ears.

Upvotes: 3

Related Questions