Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert balstack to a render function component #1313

Merged
merged 8 commits into from Jan 24, 2022
83 changes: 83 additions & 0 deletions src/components/_global/BalStack/BalStack.spec.ts
@@ -0,0 +1,83 @@
import { render } from '@testing-library/vue';
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks nice! Glad we have something to do UI tests!

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');
Comment on lines +77 to +80
Copy link
Collaborator

Choose a reason for hiding this comment

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

These tests are great! Just a minor thing here, is there any advantage to maybe using the divide class and flexgrid rather than margins and flexbox?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll look into it

});
});
});
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
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume you wanted nestedChild !== undefined && nestedChild !== null right?

Suggested change
nestedChild => nestedChild !== undefined || nestedChild !== null
nestedChild => 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 @@ -226,7 +226,7 @@ function saveAndProceed() {
</BalStack>
</AnimatePresence>
</BalStack>
<BalStack isDynamic vertical>
<BalStack vertical>
<TokenInput
v-for="(address, i) in tokenAddresses"
:key="i"
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/CreatePool/PreviewPool.vue
Expand Up @@ -156,7 +156,7 @@ function getSwapFeeManager() {
{{ $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