@@ -481,4 +481,121 @@ list: '@/renderers/ZeroStylesRichText.vue',
481481// diff-add
482482` ` `
483483
484- ` ZeroStyleRichText` fits well for tasks like email templates preview fields.
484+ ` ZeroStyleRichText` fits well for tasks like email templates preview fields.
485+
486+
487+ ### Custom filter component for square meters
488+
489+
490+ Sometimes standard filters are not enough, and you want to make a convenient UI for selecting a range of apartment areas. For example, buttons with options for “Small (<25 m²)”, “Medium (25–90 m²)” and “Large (>90 m²)”.
491+
492+ ` ` ` ts title= ' ./custom/SquareMetersFilter.vue'
493+ < template>
494+ < div class = " flex flex-col gap-2" >
495+ < p class = " font-medium mb-1 dark:text-white" > {{ $t (' Square meters filter' ) }}< / p>
496+ < div class = " flex gap-2" >
497+ < button
498+ : class = " [
499+ baseBtnClass,
500+ selected === 'small' ? activeBtnClass : inactiveBtnClass
501+ ]"
502+ @click= " select('small')"
503+ type= " button"
504+ >
505+ {{ $t (' Small' ) }}
506+ < / button>
507+ < button
508+ : class = " [
509+ baseBtnClass,
510+ selected === 'medium' ? activeBtnClass : inactiveBtnClass
511+ ]"
512+ @click= " select('medium')"
513+ type= " button"
514+ >
515+ {{ $t (' Medium' ) }}
516+ < / button>
517+ < button
518+ : class = " [
519+ baseBtnClass,
520+ selected === 'large' ? activeBtnClass : inactiveBtnClass
521+ ]"
522+ @click= " select('large')"
523+ type= " button"
524+ >
525+ {{ $t (' Large' ) }}
526+ < / button>
527+ < / div>
528+ < / div>
529+ < / template>
530+
531+ < script setup lang= " ts" >
532+ import { ref , watch } from ' vue' ;
533+
534+ const emit = defineEmits ([' update:modelValue' ]);
535+
536+ const props = defineProps< {
537+ modelValue: Array < { operator: string; value: number }> | null ;
538+ }> ();
539+
540+ const selected = ref< string | null > (null );
541+
542+ const baseBtnClass =
543+ ' flex gap-1 items-center py-1 px-3 text-sm font-medium rounded-default border focus:outline-none focus:z-10 focus:ring-4' ;
544+ const activeBtnClass =
545+ ' text-white bg-blue-500 border-blue-500 hover:bg-blue-600 focus:ring-blue-200 dark:focus:ring-blue-800' ;
546+ const inactiveBtnClass =
547+ ' text-gray-900 bg-white border-gray-300 hover:bg-gray-100 hover:text-blue-500 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700' ;
548+
549+ watch (
550+ () => props .modelValue ,
551+ (val ) => {
552+ if (! val || val .length === 0 ) {
553+ selected .value = null ;
554+ return ;
555+ }
556+ const ops = val .map ((v ) => ` ${ v .operator } :${ v .value } ` );
557+
558+ if (ops .includes (' lt:25' )) selected .value = ' small' ;
559+ else if (ops .includes (' gte:25' ) && ops .includes (' lte:90' )) selected .value = ' medium' ;
560+ else if (ops .includes (' gt:90' )) selected .value = ' large' ;
561+ else selected .value = null ;
562+ },
563+ { immediate: true }
564+ );
565+
566+ function select (size : string ) {
567+ selected .value = size;
568+
569+ switch (size) {
570+ case ' small' :
571+ emit (' update:modelValue' , [{ operator: ' lt' , value: 25 }]);
572+ break ;
573+ case ' medium' :
574+ emit (' update:modelValue' , [
575+ { operator: ' gte' , value: 25 },
576+ { operator: ' lte' , value: 90 }
577+ ]);
578+ break ;
579+ case ' large' :
580+ emit (' update:modelValue' , [{ operator: ' gt' , value: 90 }]);
581+ break ;
582+ }
583+ }
584+ < / script>
585+ ` ` `
586+
587+ ` ` ` ts title= ' ./resources/apartments.ts'
588+ columns: [
589+ ...
590+ {
591+ name: ' square_meter' ,
592+ label: ' Square' ,
593+ // diff-add
594+ components: {
595+ // diff-add
596+ filter: ' @@/SquareMetersFilter.vue'
597+ // diff-add
598+ }
599+ },
600+ ...
601+ ]
0 commit comments