Skip to content

Commit

Permalink
Convert balstack to a render function component (#1313)
Browse files Browse the repository at this point in the history
* update: convert balstack to a render function component

* fix: lint

* cleanup: remove unneeded isDynamic prop from balstack

* update: implement remaining props and add comments

* update: add balstack spec file

* update: complete balstack tests
  • Loading branch information
mrchickenburger committed Jan 24, 2022
1 parent 9a9a8f8 commit fb0118f
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 78 deletions.
83 changes: 83 additions & 0 deletions src/components/_global/BalStack/BalStack.spec.ts
@@ -0,0 +1,83 @@
import { render } from '@testing-library/vue';
import BalStack from './BalStack.vue';

describe.only('BalStack', () => {
describe('When using BalStack', () => {
it('should render items', () => {
const { getByText } = render(BalStack, {
slots: {
default: '<div>First</div><div>Second</div><div>Third</div>'
}
});

// check that elements are actually rendered as children
expect(getByText('First')).toBeVisible();
expect(getByText('Second')).toBeVisible();
expect(getByText('Third')).toBeVisible();
});

it('should render items horizontally when the horizontal prop is supplied', () => {
const { getByText } = render(BalStack, {
slots: {
default: '<div>First</div><div>Second</div><div>Third</div>'
},
props: {
horizontal: true
}
});

// its fine to make this assumption here as we render the children without any wrappers
const stackEl = getByText('First').parentElement;
expect(stackEl).toHaveClass('flex-row');
});

it('should render items verticlly if vertical prop is supplied', () => {
const { getByText } = render(BalStack, {
slots: {
default: '<div>First</div><div>Second</div><div>Third</div>'
},
props: {
vertical: true
}
});

// its fine to make this assumption here as we render the children without any wrappers
const stackEl = getByText('First').parentElement;
expect(stackEl).toHaveClass('flex-col');
});

it('should render items with space between them', () => {
const { getByText } = render(BalStack, {
slots: {
default: '<div>First</div><div>Second</div><div>Third</div>'
},
props: {
vertical: true
}
});

// the default spacing unit (tailwind) is 4. So can be either mb-4 or mr-4
expect(getByText('First')).toHaveClass('mb-4');
expect(getByText('Second')).toHaveClass('mb-4');
// last el shouldn't have a spacing class
expect(getByText('Third')).not.toHaveClass('mb-4');
});
it('should render items with a border between them if withBorder prop is supplied', () => {
const { getByText } = render(BalStack, {
slots: {
default: '<div>First</div><div>Second</div><div>Third</div>'
},
props: {
vertical: true,
withBorder: true
}
});

// the default spacing unit (tailwind) is 4. So can be either mb-4 or mr-4
expect(getByText('First')).toHaveClass('mb-4 border-b');
expect(getByText('Second')).toHaveClass('mb-4 border-b');
// last el shouldn't have a spacing class
expect(getByText('Third')).not.toHaveClass('mb-4 border-b');
});
});
});
193 changes: 123 additions & 70 deletions src/components/_global/BalStack/BalStack.vue
@@ -1,20 +1,12 @@
<script setup lang="ts">
import { uniqueId } from 'lodash';
import { computed, useSlots } from 'vue';
<script lang="ts">
import { defineComponent, PropType, h } from 'vue';
type Spacing = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | 'none';
type Props = {
vertical?: boolean;
horizontal?: boolean;
spacing?: Spacing;
withBorder?: boolean;
ref?: any;
align?: 'center' | 'start' | 'end' | 'between';
justify?: 'center' | 'start' | 'end' | 'between';
isDynamic?: boolean;
expandChildren?: boolean;
};
type Alignment = 'center' | 'start' | 'end' | 'between';
/**
* Maps discrete spacing types to a tailwind spacing tier
*/
const SpacingMap: Record<Spacing, number> = {
xs: 1,
sm: 2,
Expand All @@ -25,63 +17,124 @@ const SpacingMap: Record<Spacing, number> = {
none: 0
};
const props = withDefaults(defineProps<Props>(), {
spacing: 'base'
});
const spacingClass = computed(() => {
const spacingType = props.vertical ? 'mb' : 'mr';
return `${spacingType}-${SpacingMap[props.spacing]}`;
});
const stackId = uniqueId();
const slots = useSlots();
const slotsWithContent = computed(() => {
if (props.isDynamic) {
if (Array.isArray(slots.default()[0].children)) {
return (slots.default()[0].children as any[]).filter(
child => child.children !== 'v-if'
);
export default defineComponent({
props: {
/**
* Stacked top down
*/
vertical: { type: Boolean, default: () => false },
/**
* Stacked left to right
*/
horizontal: { type: Boolean, default: () => false },
spacing: {
type: String as PropType<Spacing>,
default: () => 'base'
},
/**
* Show a hairline border after each stack element
*/
withBorder: {
type: Boolean,
default: () => false
},
/**
* Flex align prop
*/
align: {
type: String as PropType<Alignment>
},
/**
* Flex justify prop
*/
justify: {
type: String as PropType<Alignment>
},
/**
* Will cause children of the stack to occupy
* as much space as possible.
*/
expandChildren: {
type: Boolean,
default: () => false
}
},
setup(props, { slots, attrs }) {
return {
slotsWithContent: [],
slots,
attrs
};
},
render() {
const spacingType = this.vertical ? 'mb' : 'mr';
const borderType = this.vertical ? 'b' : 'r';
const widthClass = this.expandChildren ? 'w-full' : '';
const borderClass = this.withBorder ? `border-${borderType}` : '';
const stackNodeClass = `dark:border-gray-600 ${spacingType}-${
SpacingMap[this.spacing]
} ${borderClass} ${widthClass}`;
// @ts-ignore
const vNodes = this.$slots.default() || [];
// if a childs 'value' is 'v-if', it is not visible so filter it out
// to not cause an empty node to render with margin
const children = vNodes.filter(vNode => vNode.children !== 'v-if');
// need to apply margin and decorator classes to all children
const styledChildren = children.map((child, childIndex) => {
let styledNestedChildren = child.children;
// a child can have an array of nested children, this is when
// those children are rendered as part of a 'v-for directive'
if (Array.isArray(styledNestedChildren)) {
// and those children can be nullish too
const nonNullishChildren = styledNestedChildren.filter(
nestedChild => nestedChild !== undefined || nestedChild !== null
);
styledNestedChildren = nonNullishChildren.map(
(nestedChild, nestedChildIndex) => {
//@ts-ignore
return h(nestedChild, {
class:
nestedChildIndex !== nonNullishChildren.length - 1
? stackNodeClass
: null
});
}
);
return h(
child,
{
class: childIndex !== children.length - 1 ? stackNodeClass : null
},
[styledNestedChildren]
);
}
return h(child, {
class: childIndex !== children.length - 1 ? stackNodeClass : null
});
});
return h(
'div',
{
attrs: this.$attrs,
class: [
'flex',
{
'flex-row': this.horizontal,
'flex-col': this.vertical,
'items-center': this.align === 'center',
'items-start': this.align === 'start',
'items-end': this.align === 'end',
'items-between': this.align === 'between',
'justify-center': this.justify === 'center',
'justify-start': this.justify === 'start',
'justify-end': this.justify === 'end',
'justify-between': this.justify === 'between'
}
]
},
[styledChildren]
);
}
return slots.default().filter(slot => {
if (slot.children !== 'v-if') return true;
return false;
});
});
</script>

<template>
<div
:class="[
'flex',
{
'flex-row': horizontal,
'flex-col': vertical,
'items-center': align === 'center',
'items-start': align === 'start',
'items-end': align === 'end',
'items-between': align === 'between',
'justify-center': justify === 'center',
'justify-start': justify === 'start',
'justify-end': justify === 'end',
'justify-between': justify === 'between'
}
]"
>
<component
v-for="(child, i) in slotsWithContent"
:key="`stack-${stackId}-child-${i}-${child?.key || ''}`"
:is="child"
:class="{
[spacingClass]: i !== slotsWithContent.length - 1,
'border-b': i !== slotsWithContent.length - 1 && withBorder && vertical,
'border-r':
i !== slotsWithContent.length - 1 && withBorder && horizontal,
'w-full': expandChildren,
'dark:border-gray-600': true
}"
/>
</div>
</template>
Expand Up @@ -74,7 +74,7 @@ function handleNavigate(state: StepState, stepIndex: number) {
<div class="p-4 border-b dark:border-gray-600">
<h6 class="dark:text-gray-300">{{ title }}</h6>
</div>
<BalStack vertical isDynamic spacing="base" class="p-4" justify="center">
<BalStack vertical spacing="base" class="p-4" justify="center">
<div
v-for="(step, i) in visibleSteps"
:key="`vertical-step-${step.tooltip}`"
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/CreatePool/InitialLiquidity.vue
Expand Up @@ -232,7 +232,7 @@ function saveAndProceed() {
</BalStack>
</AnimatePresence>
</BalStack>
<BalStack isDynamic vertical>
<BalStack vertical>
<TokenInput
v-for="(address, i) in tokenAddresses"
v-model:amount="seedTokens[i].amount"
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/CreatePool/PreviewPool.vue
Expand Up @@ -177,7 +177,7 @@ function getInitialWeightHighlightClass(tokenAddress: string) {
{{ $t('createAPool.tokensAndSeedLiquidity') }}
</h6>
</div>
<BalStack vertical spacing="none" withBorder isDynamic>
<BalStack vertical spacing="none" withBorder>
<div
v-for="token in seedTokens"
:key="`tokenpreview-${token.tokenAddress}`"
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/CreatePool/SimilarPools.vue
Expand Up @@ -106,7 +106,7 @@ function cancel() {
</BalStack>
</BalStack>
</BalCard>
<BalStack isDynamic v-else vertical>
<BalStack v-else vertical>
<BalCard
shadow="none"
v-for="pool in relevantSimilarPools"
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/CreatePool/SimilarPoolsCompact.vue
Expand Up @@ -33,7 +33,7 @@ function getPoolLabel(pool: Pool) {
<BalIcon class="mt-1" name="alert-circle" size="md" />
<h6>{{ $t('createAPool.similarPoolsExist') }}</h6>
</BalStack>
<BalStack vertical isDynamic spacing="sm" class="p-4">
<BalStack vertical spacing="sm" class="p-4">
<BalLink
target="_blank"
:href="`/#/pool/${pool.id}`"
Expand Down
4 changes: 2 additions & 2 deletions src/components/cards/CreatePool/WalletPoolTokens.vue
Expand Up @@ -55,7 +55,7 @@ const totalFiat = computed(() => {
<BalStack vertical class="p-4" spacing="sm">
<div>
<h6 class="branch relative">Native tokens</h6>
<BalStack isDynamic vertical spacing="xs">
<BalStack vertical spacing="xs">
<BalStack
class="ml-6 twig relative"
v-for="token in nativeTokens"
Expand Down Expand Up @@ -88,7 +88,7 @@ const totalFiat = computed(() => {
:exit="exitAnimateProps"
:isVisible="true"
>
<BalStack horizontal justify="between" isDynamic>
<BalStack horizontal justify="between">
<BalStack vertical spacing="none">
<h6>{{ _tokens[token]?.symbol || 'N/A' }}</h6>
<span class="text-sm text-gray-600">{{
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/components.ts
Expand Up @@ -6,7 +6,7 @@ export function registerGlobalComponents(app: App): void {
const req = require.context(
'@/components/_global',
true,
/^((?!stories).)*\.(js|ts|vue)$/i
/^((?!(stories|spec)).)*\.(js|ts|vue)$/i
);
for (const filePath of req.keys()) {
const componentName = parsePath(filePath).name;
Expand Down

4 comments on commit fb0118f

@vercel
Copy link

@vercel vercel bot commented on fb0118f Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on fb0118f Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on fb0118f Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on fb0118f Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.