Skip to content

Commit

Permalink
refactor: get rid of NoElement component and useNoElementRender compo…
Browse files Browse the repository at this point in the history
…sable (#1583)
  • Loading branch information
joseacabaneros authored Aug 6, 2024
1 parent f915731 commit 059eecc
Show file tree
Hide file tree
Showing 42 changed files with 401 additions and 661 deletions.
19 changes: 0 additions & 19 deletions packages/x-components/src/components/__tests__/no-element.spec.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/x-components/src/components/animations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { default as CollapseWidth } from './collapse-width.vue';
export { default as CrossFade } from './cross-fade.vue';
export { default as Fade } from './fade.vue';
export { default as FadeAndSlide } from './fade-and-slide.vue';
export { default as NoAnimation } from './no-animation.vue';
export { default as StaggeredFadeAndSlide } from './staggered-fade-and-slide.vue';
export { default as StaggeringTransitionGroup } from './staggering-transition-group.vue';
export { createDirectionalAnimationFactory } from './create-directional-animation-factory';
Expand Down
14 changes: 14 additions & 0 deletions packages/x-components/src/components/animations/no-animation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
/**
* Component to be used as `default` for animation props together with dynamic components
* `<component :is="animation">` in the template.
*/
export default defineComponent({
name: 'NoAnimation',
setup(_, { slots }) {
return () => slots.default?.()[0] ?? '';
}
});
</script>
10 changes: 3 additions & 7 deletions packages/x-components/src/components/base-dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,8 @@
import { Identifiable } from '@empathyco/x-types';
import { computed, defineComponent, nextTick, onBeforeUnmount, PropType, ref, watch } from 'vue';
import { AnimationProp } from '../types';
import { getTargetElement } from '../utils/html';
import { normalizeString } from '../utils/normalize';
import { isInRange } from '../utils/number';
import { debounce as debounceFunction } from '../utils/debounce';
import { NoElement } from './no-element';
import { debounceFunction, normalizeString, getTargetElement, isInRange } from '../utils';
import { NoAnimation } from './animations';
type DropdownItem = string | number | Identifiable;
let dropdownCount = 0;
Expand All @@ -95,7 +92,6 @@
*/
export default defineComponent({
name: 'BaseDropdown',
components: { NoElement },
props: {
/** List of items to display.*/
items: {
Expand All @@ -117,7 +113,7 @@
*/
animation: {
type: AnimationProp,
default: () => NoElement
default: () => NoAnimation
},
/** Time to wait without receiving any keystroke before resetting the items search query. */
searchTimeoutMs: {
Expand Down
3 changes: 1 addition & 2 deletions packages/x-components/src/components/display-emitter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import {
defineComponent,
getCurrentInstance,
h,
onMounted,
onUnmounted,
PropType,
Expand Down Expand Up @@ -30,7 +29,7 @@
let unwatchDisplay: WatchStopHandle | undefined;
onMounted(() => {
const element = getCurrentInstance()?.proxy.$el as HTMLElement | undefined;
const element = getCurrentInstance()?.proxy?.$el as HTMLElement | undefined;
if (element) {
unwatchDisplay = useEmitDisplayEvent({
element,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { computed, defineComponent, h, PropType, VNode, VNodeChildren } from 'vue';
import { computed, defineComponent, h, PropType, VNode } from 'vue';
import { RangeValue } from '@empathyco/x-types';
import BaseCurrency from '../../currency/base-currency.vue';
Expand Down Expand Up @@ -67,7 +67,7 @@
const render = (): VNode => {
const labelParts = label.value.split(/({min}|{max})/);
const children: VNodeChildren = labelParts.map(partMessage => {
const children = labelParts.map(partMessage => {
if (partMessage === '{min}') {
return h('BaseCurrency', {
props: {
Expand Down
155 changes: 65 additions & 90 deletions packages/x-components/src/components/highlight.vue
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
<template>
<NoElement>
<slot v-bind="{ text, hasMatch, ...matchParts }">
<span class="x-highlight" :class="dynamicCSSClasses">
<span
v-if="matchParts.start"
v-text="matchParts.start"
class="x-highlight__text"
data-test="highlight-start"
/>
<span
v-if="hasMatch"
v-text="matchParts.match"
class="x-highlight__text x-highlight-text-match"
:class="matchingPartClass"
data-test="matching-part"
/>
<span
v-if="matchParts.end"
v-text="matchParts.end"
class="x-highlight__text"
data-test="highlight-end"
/>
</span>
</slot>
</NoElement>
<span class="x-highlight" :class="dynamicCSSClasses">
<span
v-if="matchParts.start"
v-text="matchParts.start"
class="x-highlight__text"
data-test="highlight-start"
/>
<span
v-if="hasMatch"
v-text="matchParts.match"
class="x-highlight__text x-highlight-text-match"
:class="matchingPartClass"
data-test="matching-part"
/>
<span
v-if="matchParts.end"
v-text="matchParts.end"
class="x-highlight__text"
data-test="highlight-end"
/>
</span>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue';
import { normalizeString } from '../utils/normalize';
import { VueCSSClasses } from '../utils/types';
import { NoElement } from './no-element';
import { normalizeString } from '../utils';
/**
* Highlights the given part of the text. The component is smart enough to do matches
Expand All @@ -40,58 +34,59 @@
*/
export default defineComponent({
name: 'Highlight',
components: { NoElement },
props: {
/**
* The text to highlight some part of it.
*
* @public
*/
/** The text to highlight some part of it. */
text: {
type: String,
default: ''
},
/**
* The part of the text to be highlighted.
*
* @public
*/
/** The part of the text to be highlighted. */
highlight: {
type: String,
default: ''
},
/**
* CSS Class to add when the `text` string contains the `highlight` string.
*/
/** CSS Class to add when the `text` string contains the `highlight` string. */
matchClass: {
type: String,
default: ''
},
/**
* CSS Class to add when the given `text` doesn't contain the `highlight` string.
*/
/** CSS Class to add when the given `text` doesn't contain the `highlight` string. */
noMatchClass: {
type: String,
default: ''
},
/**
* CSS Class to add to the matching text.
*/
/** CSS Class to add to the matching text. */
matchingPartClass: {
type: String,
default: ''
}
},
setup: function (props) {
setup(props, { slots }) {
/**
* Splits the label in three parts based on two indexes.
*
* @param label - The string that will be divided in three parts.
* @param start - The first index that the label will be divided by.
* @param end - The second index that the label will be divided by.
*
* @returns The three parts of the divided label.
*/
function splitAt(label: string, start: number, end: number) {
return {
start: label.substring(0, start),
match: label.substring(start, end),
end: label.substring(end)
};
}
/**
* Splits the text to highlight into 3 parts: a starting part, the matching part
* and the ending part. If there is no match between the text and the highlight, the `start`
* property will contain the whole text.
*
* @returns An object containing the different parts of the text.
* @internal
*/
const matchParts = computed((): HighlightMatch => {
const matchParts = computed(() => {
const matcherIndex = normalizeString(props.text).indexOf(normalizeString(props.highlight));
return matcherIndex !== -1 && props.highlight
? splitAt(props.text.trim(), matcherIndex, matcherIndex + props.highlight.trim().length)
Expand All @@ -103,11 +98,8 @@
* matching part.
*
* @returns True if there is a match between the text and the highlight strings.
* @internal
*/
const hasMatch = computed((): boolean => {
return !!matchParts.value.match;
});
const hasMatch = computed(() => !!matchParts.value.match);
/**
* CSS classes to add depending on the component state.
Expand All @@ -117,11 +109,10 @@
* `[matchClass]`: When there is a match between the text and
* the part to highlight.
* `[noMatchClass]`: when there is no match between the text to highlight.
* @returns The {@link VueCSSClasses} classes.
* @internal
* @returns The CSS classes.
*/
const dynamicCSSClasses = computed((): VueCSSClasses => {
const classes: VueCSSClasses = {
const dynamicCSSClasses = computed(() => {
const classes = {
'x-highlight--has-match': hasMatch.value,
'x-highlight-text': hasMatch.value,
[props.matchClass]: hasMatch.value
Expand All @@ -133,45 +124,29 @@
});
/**
* Splits the label in three parts based on two indexes.
* Render function to execute the `default` slot, binding `slotsProps` and getting only the
* first `vNode` to avoid Fragments and Text root nodes.
*
* @param label - The string that will be divided in three parts.
* @param start - The first index that the label will be divided by.
* @param end - The second index that the label will be divided by.
* @remarks `slotProps` must be values without Vue reactivity and located inside the
* render-function to update the binding data properly.
*
* @returns The three parts of the divided label.
* @internal
* @returns The root `vNode` of the `default` slot.
*/
function splitAt(label: string, start: number, end: number): HighlightMatch {
return {
start: label.substring(0, start),
match: label.substring(start, end),
end: label.substring(end)
function renderDefaultSlot() {
const slotProps = {
text: props.text,
hasMatch: hasMatch.value,
...matchParts.value
};
return slots.default?.(slotProps)[0];
}
return { hasMatch, matchParts, dynamicCSSClasses };
/* Hack to render through a render-function, the `default` slot or, in its absence,
the component itself. It is the alternative for the NoElement antipattern. */
const componentProps = { hasMatch, matchParts, dynamicCSSClasses };
return (slots.default ? renderDefaultSlot : componentProps) as typeof componentProps;
}
});
/**
* Contains the different parts of a string match.
*/
interface HighlightMatch {
/**
* When the match does not happen from the beginning of the string, the initial unmatched
* part.
*/
start: string;
/**
* The part of the text that is matching.
*/
match: string;
/**
* When the match does not extend until the end, the remaining unmatched string.
*/
end: string;
}
</script>

<docs lang="mdx">
Expand Down
1 change: 0 additions & 1 deletion packages/x-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export { default as GlobalXBus } from './global-x-bus.vue';
export { default as Highlight } from './highlight.vue';
export { default as ItemsList } from './items-list.vue';
export { default as LocationProvider } from './location-provider.vue';
export { NoElement } from './no-element';
export { default as SlidingPanel } from './sliding-panel.vue';
export { default as SnippetCallbacks } from './snippet-callbacks.vue';
export { default as PageLoaderButton } from './page-loader-button.vue';
Expand Down
3 changes: 1 addition & 2 deletions packages/x-components/src/components/location-provider.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import { defineComponent, PropType, provide, toRef } from 'vue';
import { FeatureLocation } from '../types';
import { useNoElementRender } from '../composables/use-no-element-render';
/**
* Location Provider component.
Expand All @@ -26,7 +25,7 @@
const featureLocation = toRef(props, 'location');
provide('location', featureLocation);
return () => useNoElementRender(slots);
return () => slots.default?.()[0] ?? '';
}
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<template>
<BaseModal
v-bind="$attrs"
ref="baseModalEl"
@click:overlay="emitBodyClickEvent"
@focusin:body="emitBodyClickEvent"
Expand Down
Loading

0 comments on commit 059eecc

Please sign in to comment.