Skip to content

Commit

Permalink
feat: new base-slider component (#1650)
Browse files Browse the repository at this point in the history
  • Loading branch information
victorcg88 authored Nov 4, 2024
1 parent 5485d7a commit a3b1a0a
Show file tree
Hide file tree
Showing 8 changed files with 3,268 additions and 8,226 deletions.
1 change: 1 addition & 0 deletions packages/x-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"@vue/devtools-api": "~6.5.0",
"@vueuse/core": "~10.7.1",
"js-md5": "~0.8.3",
"nouislider": "~15.7.1",
"rxjs": "~7.8.0",
"tslib": "~2.6.0",
"vue-global-events": "~3.0.1"
Expand Down
337 changes: 337 additions & 0 deletions packages/x-components/src/components/base-slider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
<template>
<div class="x-base-slider">
<div ref="slider" class="x-base-slider__nouislider" />
<div class="x-base-slider__selected">
<!--
@slot Default selected range rendering. This slot will be used by default for rendering
the selected range without an specific slot implementation.
@binding {number[]} rangeSelected - The selected range values. Min position 0, Max position 1.
-->
<slot :rangeSelected="rangeSelected">
<p class="x-base-slider__selected-min">
<span>min value</span>
<span>
{{ rangeSelected[0] }}
</span>
</p>
<p class="x-base-slider__selected-max">
<span>max value</span>
<span>
{{ rangeSelected[1] }}
</span>
</p>
</slot>
</div>
</div>
</template>
<script lang="ts">
import { API, create } from 'nouislider';
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch } from 'vue';
/**
* This component implements a range slider and prints the selected values.
* It receives a threshold prop to set the limits and uses v-model to get and set
* the selected values.
*
* It makes use of the nouslider library @see https://refreshless.com/nouislider/
* for the slider implementation.
*
*/
export default defineComponent({
name: 'BaseSlider',
props: {
/** The threshold prop sets the limits for the slider. */
threshold: {
type: Object as PropType<{ min: number; max: number }>,
default: () => ({ min: 0, max: Number.MAX_SAFE_INTEGER })
},
/** The modelValue prop sets the initial values for the slider. */
modelValue: {
type: Object as PropType<{ min: number; max: number }>,
required: true
}
},
/**
* The component emits an event with the selected values whenever
* the user changes the slider.
*/
emits: ['update:modelValue'],
setup(props, { emit }) {
/** The nouislider instance. */
let sliderInstance: API;
/** The nouislider element reference. */
const slider = ref<HTMLElement>();
/** The selected min value. */
const minSelected = ref(props.modelValue?.min ?? props.threshold.min);
/** The selected max value. */
const maxSelected = ref(props.modelValue?.max ?? props.threshold.max);
/** The selected range as an array. */
const rangeSelected = computed(() => [minSelected.value, maxSelected.value]);
/** The range for the nouislider. */
const slideRange = computed(() => ({ min: props.threshold.min, max: props.threshold.max }));
onMounted(() => {
// Create the slider instance
sliderInstance = create(slider.value!, {
start: rangeSelected.value,
range: slideRange.value,
step: 1,
connect: true,
margin: 1
});
// Update the selected values when the slider update its values
sliderInstance.on('update', ([min, max]) => {
minSelected.value = Number(min);
maxSelected.value = Number(max);
});
// Emits the selected values when the slider values change
sliderInstance.on('change', () =>
emit('update:modelValue', { min: minSelected.value, max: maxSelected.value })
);
});
onUnmounted(() => {
// Waiting to finish the collapse animation before destroying it
setTimeout(sliderInstance.destroy.bind(sliderInstance), 600);
});
/**
* Watch the threshold prop to update the slider state and emit the selected values.
*/
watch(
() => props.threshold,
({ min, max }) => {
sliderInstance.updateOptions({ range: slideRange.value, start: [min, max] }, false);
emit('update:modelValue', { min, max });
}
);
/**
* Watch the modelValue prop to update the slider state.
*
* @remarks It only update the values if the values are corrects. It means,
* values within the threshold limits and not equal to the current values.
*
* @returns Undefined.
*/
watch([() => props.modelValue.min, () => props.modelValue.max], ([min, max]) => {
// Check if the values are the same
if (min === minSelected.value && max === maxSelected.value) {
return;
}
// Validate the values
const minValidated = min < props.threshold.min ? props.threshold.min : min;
const maxValidated = max > props.threshold.max ? props.threshold.max : max;
// Update the nouislider values
sliderInstance.set([minValidated, maxValidated]);
// Emit the selected values
if (minValidated !== min || maxValidated !== max) {
emit('update:modelValue', { min: minValidated, max: maxValidated });
}
});
return {
slider,
rangeSelected
};
}
});
</script>
<style lang="css">
@import 'nouislider/dist/nouislider.css';
/** Customize nouislider styles: https://refreshless.com/nouislider/examples/#section-styling */
.x-base-slider {
gap: 16px;
}
.x-base-slider,
.x-base-slider__selected-min,
.x-base-slider__selected-max {
display: flex;
flex-flow: column nowrap;
}
.x-base-slider__selected {
display: inline-flex;
}
.x-base-slider__selected-min,
.x-base-slider__selected-max {
flex: 50%;
}
.x-base-slider__nouislider {
margin: 16px 0;
padding: 0 16px;
}
.x-base-slider__nouislider .noUi-handle:before,
.x-base-slider__nouislider .noUi-handle:after {
content: none;
}
</style>
<docs lang="mdx">
## Examples

This component renders a slider and the selected values. The component needs the threshold for the
slider, although is not required (If not passed, fallback is min: 0, max: Number.MAX_SAFE_INTEGER ),
which are passed using the `threshold` prop and the selected range, which is passed in using the
v-model.

### Default usage

It is required to send the value prop which holds the selected values.

```vue live
<template>
<BaseSlider v-model="selectedRange" />
</template>

<script>
import { BaseSlider } from '@empathyco/x-components';

export default {
name: 'BaseSliderDemo',
components: {
BaseSlider
},
setup() {
const selectedRange = ref({ min: 0, max: 1000 });

return {
selectedRange
};
}
};
</script>
```

#### With threshold

```vue live
<template>
<BaseSlider v-model="selectedRange" :threshold="threshold" />
</template>

<script>
import { BaseSlider } from '@empathyco/x-components';

export default {
name: 'BaseSliderDemo',
components: {
BaseSliderDemo
},
setup() {
const threshold = ref({ min: 0, max: 1000 });
const selectedRange = ref(threshold.value);

return {
selectedRange,
threshold
};
}
};
</script>
```

### Customized usage

#### Overriding the slots

It is possible to override the default slot to customize the layout for the selected values.

```vue live
<template>
<BaseSlider v-model="selectedRange" :threshold="threshold" v-slot="{ rangeSelected }">
<p class="x-base-slider__selected-min">
<span>min value</span>
<span>
{{ rangeSelected[0] }}
</span>
</p>
<p class="x-base-slider__selected-max">
<span>max value</span>
<span>
{{ rangeSelected[1] }}
</span>
</p>
</BaseSlider>
</template>

<script>
import { BaseSlider } from '@empathyco/x-components';

export default {
name: 'BaseSliderDemo',
components: {
BaseSliderDemo
},
setup() {
const threshold = ref({ min: 0, max: 1000 });
const selectedRange = ref(threshold.value);

return {
selectedRange,
threshold
};
}
};
</script>
```

It is also possible to add inputs to complement the slider and allow to change the selected values
manually.

```vue live
<template>
<BaseSlider v-model="selectedRange" :threshold="threshold">
<input
@change="selectedRange.min = $event.target?.valueAsNumber || 0"
class="x-input"
name="min"
type="number"
:value="selectedRange.min"
:aria-label="'min'"
/>

<input
@change="selectedRange.max = $event.target?.valueAsNumber || 1000000"
style="display: block"
class="x-input"
name="max"
type="number"
:value="selectedRange.max"
:aria-label="'max'"
/>
</BaseSlider>
</template>

<script>
import { BaseSlider } from '@empathyco/x-components';

export default {
name: 'BaseSliderDemo',
components: {
BaseSliderDemo
},
setup() {
const threshold = ref({ min: 0, max: 1000 });
const selectedRange = ref(threshold.value);

return {
selectedRange,
threshold
};
}
};
</script>
```
</docs>
1 change: 1 addition & 0 deletions packages/x-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { default as BaseEventButton } from './base-event-button.vue';
export { default as BaseGrid } from './base-grid.vue';
export { default as BaseKeyboardNavigation } from './base-keyboard-navigation.vue';
export { default as BaseRating } from './base-rating.vue';
export { default as BaseSlider } from './base-slider.vue';
export { default as BaseSwitch } from './base-switch.vue';
export { default as BaseVariableColumnGrid } from './base-variable-column-grid.vue';
export { default as DisplayEmitter } from './display-emitter.vue';
Expand Down
5 changes: 5 additions & 0 deletions packages/x-components/src/composables/use-alias-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export function useAliasApi(): UseAliasAPI {
},
get selectedSort() {
return store.state.x.search?.sort ?? '';
},
get priceStats() {
return store.state.x.search?.stats?.price ?? {};
}
};
}
Expand Down Expand Up @@ -213,6 +216,8 @@ export interface UseAliasAPI {
readonly totalResults: number;
/** The {@link SearchXModule} selected sort. */
readonly selectedSort: string;
/** The {@link SearchXModule} price specific stats. */
readonly priceStats: { min: number; max: number };
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Stats } from '@empathyco/x-types';
import { SearchXStoreModule } from '../types';

/**
Expand All @@ -21,7 +22,8 @@ export const saveSearchResponse: SearchXStoreModule['actions']['saveSearchRespon
spellcheck,
redirections,
queryTagging,
displayTagging
displayTagging,
stats
}
) => {
if (totalResults === 0) {
Expand Down Expand Up @@ -58,4 +60,5 @@ export const saveSearchResponse: SearchXStoreModule['actions']['saveSearchRespon

commit('setTotalResults', totalResults);
commit('setSpellcheck', spellcheck ?? '');
commit('setStats', stats as Stats);
};
Loading

0 comments on commit a3b1a0a

Please sign in to comment.