Reputation: 1194
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
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