@econnect/webcomponents-library
Documentation
Styleguide
Documentation
Styleguide
  • Getting Started

    • Installation
    • Patch Notes
    • Styleguide
  • Styling

    • e-connect colors
  • UI Components

    • Form

      • Search Field
      • Form Field
      • Forms
    • Filter

      • Filter
      • Search Filter
    • Grid

      • Grid
      • Grid Row
    • Inputs

      • File Input
      • Input Checkbox
      • Input Dropdown
      • Input Date
      • Input Date Time
      • Input Date Time Local
      • Input File
      • Input Label
      • Input Multi Select
      • Input Number
      • Input Radio
      • Input Range
      • Input Select
      • Input Switch
      • Input Slider
      • Input Text
      • Input TextArea
      • Input Time
      • Input Time Range
    • Controls

      • Booleans
      • Button
      • Checkboxes
      • Date Displays
      • Dropdown Lists
      • Text Displays
    • Headers

      • e-header
    • Cards

      • e-card
    • Images

      • Icon
      • Images
      • Flag Image
      • Hamburger
    • Iframe
    • Pager
    • Settings
    • Document Validation
    • Navigation

      • Navigation
      • Navigation Menu
      • Navigation Footer
      • Navigation Button
      • Navigation Button Item
      • Navigation Search Item

Grid v1.0

The <e-grid> component is a component that can be used for displaying a variety of grids.

Usage

An e-grid has attributes that must be provided.

<e-grid
  :loading="grid.loading"
  :items="grid.items"
  :grid-model="grid.model"
  :page="grid.currentPage"
  :total="grid.totalItemCount"
  :query-parameters="urlParams"
  @row-clicked="rowClicked"
  @column:resize="columnResized"
  @column:switch="columnSwitched"
  @emit-key="gridEvents"
/>

Attributes

gridModel | Type: GridData | required
loading | Type: Boolean | Default: false
items | Type: Object[] | Default: []
errors | Type: Object[] | Default: []
queryParameters | Type: QueryParameters | Default: {}
total | Type: Number | Default: 0
page | Type: Number | Default: 0
pagerDisplayOffset | Type: Number | Default: 2

Examples

id

name

isActive

tags

parties

changedOn

createdOn

1
test 1
test1
Placeholder image

test party

party
Placeholder image

test party 2

party 2
10/10/20251 second ago
10/10/20251 second ago
2
test 2
test1
test2
10/10/20251 second ago
10/10/20251 second ago
3
test 3
test1
test2
test3
10/10/20251 second ago
10/10/20251 second ago
4
test 4
test1
10/10/20251 second ago
10/10/20251 second ago
5
test 5
test1
test3
10/10/20251 second ago
10/10/20251 second ago
6
test 6
10/10/20251 second ago
10/10/20251 second ago
7
test 7
test1
test2
test3
10/10/20251 second ago
10/10/20251 second ago
8
test 8
10/10/20251 second ago
10/10/20251 second ago
9
test 9
test2
10/10/20251 second ago
10/10/20251 second ago
10
test 10
test3
10/10/20251 second ago
10/10/20251 second ago

Source Code

e-grid.vue
<script setup lang="ts">
import { computed, provide, ref, watch } from 'vue';
import { isEmpty, filter, orderBy, size, chunk, some } from 'lodash-es';
import { useI18n } from 'vue-i18n';

import { ColumnModel, GridColumnTypes, GridData, GridEmit, QueryParameters } from 'types/grid';
import { Dictionary } from 'types/Dictionary';

import EGridSupportWarning from '../e-grid-support-warning/e-grid-support-warning.vue';
import EGridHeader from '../e-grid-header/e-grid-header.vue';
import EGridError from '../e-grid-error/e-grid-error.vue';
import EGridRow from '../e-grid-row/e-grid-row.vue';
import EGridFooter from '../e-grid-footer/e-grid-footer.vue';
import ELoadingIndicator from '../e-loading-indicator/e-loading-indicator.vue';

export type Props = {
  gridModel: GridData;
  loading?: boolean;
  items?: object[];
  errors?: string[];
  queryParameters?: QueryParameters;
  total?: number;
  page?: number;
  pagerDisplayOffset?: number;
  settings?: Dictionary & {
    activeOnly: boolean;
  };
};
const props = withDefaults(defineProps<Props>(), {
  loading: false,
  items: () => {
    return [];
  },
  errors: () => {
    return [];
  },
  queryParameters: () => {
    return {
      sort: [],
      filter: [],
      skip: 0,
      take: 0
    };
  },
  total: 0,
  page: 0,
  pagerDisplayOffset: 2,
  settings: () => {
    return {
      activeOnly: true
    };
  }
});

const emit = defineEmits<{
  (e: 'click', column: ColumnModel): void;
  (e: 'column:resize', column: ColumnModel): void;
  (e: 'column:switch', originalColumn: ColumnModel, targetColumn: ColumnModel): void;
  (e: 'emit-key', emitKey: string, rowData: GridEmit, functionMeta?: object): void;
  (e: 'grid:reload', params: QueryParameters): void;
  (e: 'row-clicked', row: object): void;
  (e: 'sorted', column: ColumnModel): void;
}>();

const { t } = useI18n({ useScope: 'global' });

provide(
  'grid-id',
  computed(() => currentGridModel.value.id)
);
provide(
  'grid-loading-state',
  computed(() => props.loading)
);
provide(
  'grid-size',
  computed(() => {
    return {
      rowHeight: rowHeight.value,
      contentHeight: contentHeight.value
    };
  })
);
provide(
  'grid-model',
  computed(() => currentGridModel.value)
);
provide(
  'grid-columns',
  computed(() => gridColumns.value)
);
provide(
  'grid-count:prop',
  computed(() => itemCount.value)
);
provide(
  'grid-count:total',
  computed(() => totalItemCount.value)
);
provide(
  'grid-count:pages',
  computed(() => pageCount.value || 1)
);
provide(
  'grid-page',
  computed(() => currentPage.value)
);
provide(
  'grid-page-size',
  computed(() => pageSize.value || 1)
);
provide(
  'grid-query-params',
  computed(() => urlParams.value)
);

const isLoading = computed(() => {
  return props.loading;
});

const hasError = computed(() => {
  return !isEmpty(props.errors);
});

const contentClasses = computed(() => {
  return {
    loading: isLoading.value,
    error: hasError.value
  };
});

const gridSettings = computed(() => {
  return props.settings;
});

const urlParams = computed(() => {
  return props.queryParameters;
});

const pageSize = computed(() => {
  return urlParams.value.take;
});

const currentPage = computed(() => {
  return props.page;
});

const currentGridModel = computed(() => {
  return props.gridModel;
});

const activeColumns = computed(() =>
  filter(currentGridModel.value?.columns || {}, column => column.active)
);

const gridColumns = computed(() => {
  if (gridSettings.value.activeOnly) return orderBy(activeColumns.value, ['index'], ['asc']);

  return orderBy(currentGridModel.value?.columns || {}, ['index'], ['asc']);
});

const hasItems = computed(() => !isEmpty(props.items));

const itemCount = computed(() => size(props.items) || 0);

const totalItemCount = computed(() => {
  return props.total || itemCount.value;
});

const pageCount = computed(() => {
  if (totalItemCount.value && pageSize.value)
    return Math.ceil(totalItemCount.value / pageSize.value);

  return Math.max(1, size(pageData.value));
});

const pageData = computed(() => {
  const dataList = hasItems.value ? props.items : [];

  return chunk(dataList, pageSize.value);
});

const currentPageGridRowData = computed(() => {
  if (pageData.value[currentPage.value]) return pageData.value[currentPage.value] as object[];

  return (pageData.value?.[0] || []) as object[];
});

const manualRowHeight = ref(40);
const useDynamicHeight = ref(false);

const detectedBrowserAgent = computed(() => {
  if (typeof window !== 'undefined') {
    if (window) {
      // Get the user-agent string
      let userAgentString = navigator.userAgent;

      // Detect Chrome
      let chromeAgent = userAgentString.indexOf('Chrome') > -1;

      // Detect Internet Explorer
      let IExplorerAgent =
        userAgentString.indexOf('MSIE') > -1 || userAgentString.indexOf('rv:') > -1;

      // Detect Firefox
      let firefoxAgent = userAgentString.indexOf('Firefox') > -1;

      // Detect Safari
      let safariAgent = userAgentString.indexOf('Safari') > -1;

      // Discard Safari since it also matches Chrome
      if (chromeAgent && safariAgent) safariAgent = false;

      // Detect Opera
      let operaAgent = userAgentString.indexOf('OP') > -1;

      // Discard Chrome since it also matches Opera
      if (chromeAgent && operaAgent) chromeAgent = false;

      return {
        safariAgent: safariAgent,
        chromeAgent: chromeAgent,
        IExplorerAgent: IExplorerAgent,
        operaAgent: operaAgent,
        firefoxAgent: firefoxAgent
      };
    }
  }

  return {
    safariAgent: false,
    chromeAgent: false,
    IExplorerAgent: false,
    operaAgent: false,
    firefoxAgent: false
  };
});

const supportWarning = computed(() => {
  const result = [];

  if (detectedBrowserAgent.value.safariAgent) result.push(t('grid.warning.support', ['Safari']));

  return result;
});

const rowHeight = computed(() => {
  // TODO: refined row height requirement checking.
  // const partyColumns = filter(gridColumns.value, { 'type': GridColumnTypes.parties })
  // if (!isEmpty(partyColumns)) {
  //   if (some(partyColumns, column => checkRequirements(column, partyRequirements.value)))
  //     return 120
  // }

  if (some(gridColumns.value, { type: GridColumnTypes.parties })) return 100;

  if (some(gridColumns.value, { type: GridColumnTypes.tags })) return 60;

  return manualRowHeight.value;
});

const contentHeight = computed(() => {
  let rowCount = pageSize.value;

  if (useDynamicHeight.value) {
    rowCount = Math.min(pageSize.value, currentPageGridRowData.value?.length);
  }

  return rowCount * rowHeight.value;
});

const contentStyle = computed(() => {
  return {
    height: contentHeight.value + 'px'
  };
});

function rowClicked(rowData: object) {
  emit('row-clicked', rowData);
}

function emitKey(emitKey: string, rowData: GridEmit, functionMeta: object = undefined) {
  emit('emit-key', emitKey, rowData, functionMeta);
}

function resizeColumn(updatedColumn: ColumnModel) {
  emit('column:resize', updatedColumn);
}

function switchColumnIndex(initialColumn: ColumnModel, targetColumn: ColumnModel) {
  emit('column:switch', initialColumn, targetColumn);
}

function handleSort(targetColumn: ColumnModel) {
  emit('sorted', targetColumn);
}

function reload(params: QueryParameters) {
  emit('grid:reload', params);
}

function handleEmit(emitKey: string, target: string | number | ColumnModel) {
  emit('emit-key', emitKey, target);
}

const scrollTarget = ref<HTMLElement>();
function scrollToTop() {
  // Due to sticky header, instantly scroll to an offset scroll target.
  const scrollTargetElement = scrollTarget.value;
  scrollTargetElement?.scrollIntoView({ behavior: 'auto' });
}

const gridContent = ref<HTMLDivElement>();
const gridContentWrapper = ref<HTMLDivElement>();
const gridErrors = ref<HTMLDivElement>();
function catchScroll() {
  const content = gridContent.value;
  const contentWrapper = gridContentWrapper.value;
  const scrollLeft = contentWrapper?.scrollLeft || 0;

  if (content && content.style) {
    content.style.left = `${scrollLeft}px`;

    content.scrollTo({
      left: scrollLeft,
      top: content?.scrollTop,
      behavior: 'instant'
    });
  }
}

function resetScrolling() {
  gridContent.value.scrollTo({
    left: 0,
    top: gridContent.value?.scrollTop,
    behavior: 'instant'
  });
  gridContentWrapper.value.scrollTo({
    left: 0,
    top: gridContentWrapper.value?.scrollTop,
    behavior: 'instant'
  });
}

watch(
  () => props.loading,
  async newValue => {
    if (newValue) {
      // When loading starts, scroll to top.
      scrollToTop();
      catchScroll();
    }
  }
);

watch(
  () => currentPage.value,
  () => {
    resetScrolling();
  }
);
</script>

<template>
  <div class="e-grid">
    <div
      v-if="detectedBrowserAgent.safariAgent"
      class="support-warning"
    >
      <e-grid-support-warning :warnings="supportWarning" />
    </div>

    <template v-else>
      <div class="grid-scroll-container">
        <div
          ref="gridContentWrapper"
          class="grid-content-wrapper"
          :class="contentClasses"
          @scroll="catchScroll"
        >
          <div class="grid-header">
            <e-grid-header
              @column:resize="resizeColumn"
              @column:switch="switchColumnIndex"
              @sorted="handleSort"
            />
          </div>

          <e-grid-error
            v-if="hasError"
            ref="gridErrors"
            :errors="errors"
          />

          <div
            ref="gridContent"
            class="grid-content"
            :class="contentClasses"
            :style="contentStyle"
          >
            <div
              ref="scrollTarget"
              class="scroll-target"
              aria-hidden="true"
            />
            <e-loading-indicator v-if="loading" />

            <template v-else-if="hasItems">
              <e-grid-row
                v-for="(rowData, rowIndex) in currentPageGridRowData"
                :key="rowIndex"
                :row-data="rowData"
                @click="rowClicked(rowData)"
                @emit-key="emitKey"
              />
            </template>
            <div
              v-else
              class="no-results"
            >
              <h3
                class="no-results-text"
                v-text="$t('grid.noResults')"
              />
            </div>
          </div>
        </div>
      </div>

      <div class="grid-footer">
        <client-only>
          <e-grid-footer
            :current-page="currentPage"
            :display-offset="pagerDisplayOffset"
            @paging="reload"
            @emit-key="handleEmit"
          />

          <template #fallback>
            <e-loading-indicator />
          </template>
        </client-only>
      </div>
    </template>
  </div>
</template>

<style scoped lang="scss">
$grid-header-height: 70px;
$grid-footer-height: 70px;
$error-window-height: 100px;

.e-grid {
  background-color: var(--e-background);
  border-radius: 15px;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: calc(75px + $grid-header-height + $grid-footer-height);
  width: 100%;
  flex: 1 1 auto;

  .support-warning {
    padding: 20px;
  }

  .grid-scroll-container {
    width: 100%;
    overflow: hidden;
    border-radius: 15px 15px 0 0;
    position: relative;
    flex: 1 1 auto;
    min-height: fit-content;

    .scroll-target {
      position: absolute;
      top: 0;
    }

    .grid-content-wrapper {
      background-color: var(--e-grid-background);
      display: block;
      position: absolute;
      overflow-x: auto;
      overflow-y: hidden;
      right: 0;
      top: 0;
      bottom: 0;
      left: 0;
      padding-top: $grid-header-height;
    }

    &::-webkit-scrollbar-track {
      background-color: var(--e-white);

      &:vertical {
        border-radius: 0 15px 0 0;
      }
    }
  }

  .grid-header,
  .grid-content {
    position: absolute;
    left: 0;
    min-width: 100%;
  }

  .grid-header {
    top: 0;
    overflow: hidden;
    z-index: 2;
  }

  .grid-content {
    position: absolute;
    display: block;
    flex-direction: column;
    z-index: 1;
    transition:
      height 0.2s ease-in-out,
      left 0s,
      max-height 0.3s ease-in-out,
      top 0.3s ease-in-out,
      width 0s;
    top: $grid-header-height;
    height: fit-content;
    overflow-x: hidden;
    overflow-y: auto;
    bottom: 0;
    max-height: calc(100% - $grid-header-height);
    width: 100%;
    max-width: 100%;

    .loading-indicator {
      margin: auto;
    }

    &.loading {
      display: flex;
      justify-content: center;
      flex-direction: column;
      align-items: center;
      width: 100%;
    }

    .no-results {
      display: flex;
      justify-content: center;

      .no-results-text {
        color: var(--e-secondary);
      }
    }

    &.error {
      top: calc($error-window-height + $grid-header-height);
      max-height: calc(100% - $error-window-height - $grid-header-height);
    }
  }

  .e-grid-error-window {
    height: $error-window-height;
  }

  .grid-header {
    height: $grid-header-height;
  }

  .grid-footer {
    height: $grid-footer-height;
  }
}

@media only screen and (max-width: 1200px) {
  .e-grid {
    height: auto;

    .grid-footer {
      height: fit-content;
    }
  }
}
</style>
Last Updated:: 10/10/24, 2:56 PM
Contributors: Roeland Krijgsman, Marcel Lommers
Next
Grid Row