@spectric/ui 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,84 +9,143 @@ export const TableElementTag = "spectric-table"
9
9
  import { spreadProps } from '../../utils/spread';
10
10
  import { PaginationChangeProps, PaginationProps } from '../pagination';
11
11
  import { FilterEvent } from './cell';
12
+ import { createSortChain } from './sorting';
12
13
  export type { TableProps, TableEvents }
13
14
 
14
15
  export type DomRenderable = HTMLElement | TemplateResult | string | number | null
16
+ export enum TableSelectOptions {
17
+ multi = "multi",
18
+ single = "single",
19
+ none = "none"
20
+ }
15
21
 
22
+ export enum TableSortOption {
23
+ multi = "multi",
24
+ single = "single",
25
+ }
26
+ export type TableSortOptionTypes = `${TableSortOption}`
27
+ export enum TableSortDirection {
28
+ ascending = "ascending",
29
+ decending = "decending",
30
+ none = "none"
31
+ }
32
+ export type TableSortDirectionTypes = `${TableSortDirection}`
16
33
  export type ColumnSettings<T> = {
17
34
  width?: number
18
35
  whiteSpace?: "nowrap";
19
36
  hidden?: boolean
20
37
  sortable?: boolean
38
+ sortDirection?: TableSortDirectionTypes
21
39
  filterable?: boolean
22
40
  title?: DomRenderable
41
+ /**
42
+ * Key to used for getting data from an object for a cell
43
+ */
23
44
  key?: string
45
+ /**
46
+ * Render function to render a table cell for displaying custom html
47
+ */
24
48
  render?: (row: T, table: TableElement<T>) => DomRenderable
49
+ /**
50
+ * Custom comparator function for sorting
51
+ */
52
+ compareFn?: ((a: T, b: T) => number) | undefined
25
53
  }
26
- type TableSelectOptions = "multi" | "single"
27
- interface TableProps<T> {
54
+ export type TableSelectOptionsTypes = `${TableSelectOptions}`
55
+ export interface TableDataOptions<T> {
28
56
  pagination?: PaginationProps
29
57
  columns: ColumnSettings<T>[]
58
+ }
59
+ interface TableProps<T> extends TableDataOptions<T> {
30
60
  data: T[]
31
- select?: TableSelectOptions
61
+ select: TableSelectOptionsTypes
62
+ sort?: TableSortOptionTypes
32
63
  }
33
64
 
34
65
  type DomEvent<T> = Event & {
35
66
  target: T
36
67
  }
37
68
  /**
38
- * Table Element
39
- *
40
- * The table element is a bit more complex and the column settings and data can only be set through the properties
41
- *
42
- *
43
- * React
44
- *
45
- * ``` tsx
46
- * <spectric-table data={[{col1:1}]} columns={[{key:"col1",}]} ></spectric-table>
47
- * ```
48
- *
49
- * Javascript
50
- *
51
- * ``` js
52
- * table = document.createElement("spectric-table")
53
- * table.data = [{col1:1}]
54
- * table.columns = [{key:"col1",}]
55
- * ```
56
- *
57
- * HTML
58
- *
59
- * ``` html
60
- * <spectric-table id="table"></spectric-table>
61
- * <script>
62
- * document.querySelector("#table")
63
- * table.data = [{col1:1}]
64
- * table.columns = [{key:"col1",}]
65
- * </script>
66
- * ```
69
+ * React example
70
+ * <iframe width="100%" height="400px" src="https://stackblitz.com/edit/react-ts-2ue7azag?ctl=1&embed=1&file=App.tsx&hideExplorer=1&hideNavigation=1"/>
71
+ *
67
72
  */
68
73
  @customElement(TableElementTag)
69
74
  export class TableElement<T> extends LitElement implements TableProps<T> {
70
75
  @property({ type: Array, attribute: false })
71
76
  data: T[] = [];
72
77
  @property({ type: Object, attribute: false })
73
- pagination?: PaginationProps | undefined;
78
+ pagination?: PaginationProps;
74
79
  @property({ attribute: false })
75
80
  columns: ColumnSettings<T>[] = [];
76
- @property({ type: String, reflect: false })
77
- select?: TableSelectOptions;
81
+ @property({ type: String, reflect: true })
82
+ select: TableSelectOptionsTypes = TableSelectOptions.none;
83
+ @property({ type: String, reflect: true })
84
+ sort: TableSortOptionTypes = TableSortOption.single;
85
+
86
+ static getDefaultDataSorterAndPaginatior<T>(data: T[]) {
87
+ return (props: TableDataOptions<T>) => {
88
+ let sorts = props.columns.filter(column => column.sortable && column.sortDirection && column.sortDirection !== TableSortDirection.none)
89
+ let rows = [...data] // Need to copy the array to prevent mutating the ordering of the original data
90
+ if (sorts.length) {
91
+ let sortChain = createSortChain(sorts)
92
+ rows.sort((a, b) => {
93
+ for (let sort of sortChain) {
94
+ let result = sort(a, b)
95
+ if (result) {
96
+ return result
97
+ }
98
+ }
99
+ return 0
100
+ })
101
+ }
102
+
103
+ if (!props.pagination) {
104
+ return rows
105
+ }
106
+ let { page, pageSize } = props.pagination
107
+ if (!page || !pageSize) {
108
+ return rows
109
+ }
110
+ return !props.pagination ? rows : rows.slice((page - 1) * pageSize, (page) * pageSize)
111
+ }
112
+ }
113
+
78
114
  private _handlePaginationChange = (e: CustomEvent<PaginationChangeProps>) => {
79
115
  e.preventDefault()
80
116
  e.stopPropagation()
81
117
  if (this.pagination) {
82
- this.pagination.size
83
- this.pagination = { ...this.pagination, ...e.detail }
118
+ let pagination = { ...this.pagination, ...e.detail }
119
+
120
+ let { totalItems, page, pageSize } = pagination
121
+ if (totalItems && page && pageSize && ((page - 1) * pageSize) > totalItems) {
122
+ pagination.page = 1
123
+ }
124
+ this.pagination = pagination
84
125
  }
85
126
  this._emitChange()
86
127
  };
128
+ private _handleSortChange = (e: CustomEvent<ColumnSettings<T>>) => {
129
+ let columnSetting = e.detail
130
+ let column = this.columns.find(col => col.key == columnSetting.key)
131
+ if (!column) {
132
+ console.warn("Unable to find sort column")
133
+ return
134
+ }
135
+ if (this.sort == TableSortOption.single) {
136
+ //Single column sort so we reset the sort direction for all columns
137
+ this.columns.forEach(col => {
138
+ col.sortDirection = TableSortDirection.none
139
+ })
140
+ }
141
+ column.sortDirection = columnSetting.sortDirection;
142
+ this.columns = [...this.columns]
143
+ this._emitChange()
144
+ }
145
+
87
146
  private _emitChange = () => {
88
- let { pagination } = this
89
- this.dispatchEvent(new CustomEvent<{ pagination?: PaginationChangeProps }>("change", { detail: { pagination } }))
147
+ let { pagination, columns } = this
148
+ this.dispatchEvent(new CustomEvent<TableDataOptions<T>>("change", { detail: { pagination, columns } }))
90
149
  }
91
150
  //@ts-ignore
92
151
  private __DO_NOT_USE_filter = () => {
@@ -94,7 +153,7 @@ export class TableElement<T> extends LitElement implements TableProps<T> {
94
153
  this.dispatchEvent(new CustomEvent<FilterEvent<T>>("filter"))
95
154
  }
96
155
  @state()
97
- selected: T[] = [];
156
+ private selected: T[] = [];
98
157
  protected createRenderRoot(): HTMLElement | DocumentFragment {
99
158
  return this
100
159
  }
@@ -105,27 +164,27 @@ export class TableElement<T> extends LitElement implements TableProps<T> {
105
164
  } else {
106
165
  this.selected = []
107
166
  }
108
- this.dispatchEvent(new CustomEvent("select", { detail: this.selected }))
167
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selected }))
109
168
  }
110
169
  protected render(): unknown {
111
170
  let columns = this.columns.filter(column => !column.hidden)
112
- if (this.select) {
171
+ if (this.select !== TableSelectOptions.none) {
113
172
  columns.unshift({
114
173
  title: this.select === "multi" ? html`<spectric-input variant="checkbox" @change=${this._handleSelectAllChange} .helperText=${"Select All"}></spectric-input>` : null,
115
174
  render: (row) => {
116
- return html`<spectric-input variant="checkbox" .checked=${this.selected.includes(row)} @change=${(e: DomEvent<HTMLInputElement>) => {
175
+ return html`<spectric-input variant="checkbox" class="table-checkbox-${this.select}" .checked=${this.selected.includes(row)} @change=${(e: DomEvent<HTMLInputElement>) => {
117
176
  e.stopPropagation()
118
177
  if (this.select === "single") {
119
178
  this.selected = []
120
179
  }
121
180
  if (e.target.checked) {
122
181
  this.selected.push(row)
123
- this.dispatchEvent(new CustomEvent("select", { detail: this.selected }))
182
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selected }))
124
183
  } else {
125
184
  let index = this.selected.findIndex(value => value === row)
126
185
  if (index !== -1) {
127
186
  this.selected.splice(index, 1)
128
- this.dispatchEvent(new CustomEvent("select", { detail: this.selected }))
187
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selected }))
129
188
  }
130
189
  }
131
190
  }}></spectric-input>`
@@ -134,9 +193,11 @@ export class TableElement<T> extends LitElement implements TableProps<T> {
134
193
  }
135
194
 
136
195
  return html`
137
- <div role="table">
138
- <spectric-table-header .columns=${columns}></spectric-table-header>
139
- <spectric-table-body .columns=${columns} .data=${this.data} .table=${this}></spectric-table-body>
196
+ <div class="table-wrapper">
197
+ <div role="table">
198
+ <spectric-table-header .columns=${columns} @sortChange=${this._handleSortChange}></spectric-table-header>
199
+ <spectric-table-body .columns=${columns} .data=${this.data} .table=${this}></spectric-table-body>
200
+ </div>
140
201
  </div>
141
202
  ${this.pagination ? html`<spectric-pagination ${spreadProps(this.pagination)} @change=${this._handlePaginationChange}></spectric-pagination>` : null}
142
203
  `;
@@ -156,7 +217,7 @@ declare global {
156
217
  namespace JSX {
157
218
  interface IntrinsicElements {
158
219
  /**
159
- * @see {@link DialogElement}
220
+ * @see {@link TableElement}
160
221
  */
161
222
  [TableElementTag]: ReactElementWithPropsAndEvents<TableElement<any>, TableProps<any>, TableEvents>;
162
223
  }
@@ -165,7 +226,7 @@ declare global {
165
226
  namespace JSX {
166
227
  interface IntrinsicElements {
167
228
  /**
168
- * @see {@link DialogElement}
229
+ * @see {@link TableElement}
169
230
  */
170
231
  [TableElementTag]: ReactElementWithPropsAndEvents<TableElement<any>, TableProps<any>, TableEvents>
171
232
  }
@@ -210,7 +210,7 @@ declare global {
210
210
  namespace JSX {
211
211
  interface IntrinsicElements {
212
212
  /**
213
- * @see {@link DialogElement}
213
+ * @see {@link TooltipElement}
214
214
  */
215
215
  [TooltipElementTag]: ReactElementWithPropsAndEvents<TooltipElement, TooltipProps, TooltipEvents>;
216
216
  }
@@ -219,7 +219,7 @@ declare global {
219
219
  namespace JSX {
220
220
  interface IntrinsicElements {
221
221
  /**
222
- * @see {@link DialogElement}
222
+ * @see {@link TooltipElement}
223
223
  */
224
224
  [TooltipElementTag]: ReactElementWithPropsAndEvents<TooltipElement, TooltipProps, TooltipEvents>
225
225
  }
@@ -1,5 +1,5 @@
1
1
 
2
- import { filterByColumn, tablecolumns, tabledata } from "./data";
2
+ import { filterByColumn, tablecolumns, tabledata, TestData } from "./data";
3
3
  import { ExampleBits } from "./Bits";
4
4
  import { FieldTypes, SpectricQuery } from "../../components/query_bar/QueryBar";
5
5
 
@@ -7,8 +7,9 @@ import { html, LitElement } from 'lit';
7
7
 
8
8
  import { customElement, property, query, state } from 'lit/decorators.js';
9
9
  import "./lorumipsum"
10
- import { PaginationChangeProps } from "../../components/pagination";
10
+ import { PaginationChangeProps, PaginationProps } from "../../components/pagination";
11
11
  import { FilterEvent } from "../../components/table/cell";
12
+ import { TableDataOptions, TableElement } from "../../components/table";
12
13
  type Props = {
13
14
  frameWidth: number
14
15
  }
@@ -23,10 +24,15 @@ export class SpectricStorybookExampleContent extends LitElement implements Props
23
24
  @query("spectric-query")
24
25
  query!: SpectricQuery
25
26
 
27
+ private dataSorter = TableElement.getDefaultDataSorterAndPaginatior<TestData>(tabledata)
26
28
  @state()
27
29
  dialogOpen: boolean = false;
28
- tableData = tabledata.slice(0, 3)
29
- pagination = {
30
+ get tableData() {
31
+ return this.dataSorter(this)
32
+ }
33
+
34
+ columns = tablecolumns
35
+ pagination: PaginationProps = {
30
36
  page: 1,
31
37
  pageSize: 3,
32
38
  size: "xsmall",
@@ -35,17 +41,17 @@ export class SpectricStorybookExampleContent extends LitElement implements Props
35
41
  _handleFilter = (e: CustomEvent<FilterEvent<any>>) => {
36
42
  let include = e.detail.include ? "" : "not "
37
43
  if (e.detail.column.key && e.detail.value) {
38
- if (this.query.value !== "") {
44
+ if (this.query.value.trim() !== "") {
39
45
  this.query.value = `${this.query.value} and ${include}${e.detail.column.key}: '${e.detail.value}'`
40
46
  } else {
41
47
  this.query.value = `${include}${e.detail.column.key}: '${e.detail.value}'`
42
48
  }
43
49
  }
44
50
  }
45
- _handlePaginationChange = (e: CustomEvent<{ pagination: PaginationChangeProps }>) => {
46
- let { pagination } = e.detail
51
+ _handlePaginationChange = (e: CustomEvent<TableDataOptions<TestData>>) => {
52
+ let { pagination, columns } = e.detail
47
53
  this.pagination = { ...this.pagination, ...pagination }
48
- this.tableData = tabledata.slice((pagination.page - 1) * pagination.pageSize, (pagination.page) * pagination.pageSize)
54
+ this.columns = columns
49
55
  this.requestUpdate()
50
56
  }
51
57
  render() {
@@ -89,6 +95,7 @@ export class SpectricStorybookExampleContent extends LitElement implements Props
89
95
 
90
96
  </spectric-query>
91
97
  <spectric-table
98
+ style="height:200px;"
92
99
  .data=${this.tableData}
93
100
  .columns=${tablecolumns}
94
101
  @filter=${this._handleFilter}
@@ -33,19 +33,32 @@ export const filterByColumn = async (field, text) => {
33
33
  return values.filter(v => v.includes(text))
34
34
  }
35
35
 
36
- type TestData = {
36
+ export type TestData = {
37
37
  name: string
38
38
  company: string
39
39
  contact: string
40
- country: string
40
+ location: {
41
+ country: string,
42
+ state: string
43
+ },
44
+ years: number
41
45
  }
42
-
43
46
  export const tabledata: TestData[] = [
44
- { name: "Sean", company: "Spectric Labs", "contact": "123-4567", "country": "US" },
45
- { name: "Kipp", company: "Spectric Labs", "contact": "123-4567", "country": "UK" },
46
- { name: "Adam", company: "Spectric Labs", "contact": "123-4567", "country": "US" },
47
- { name: "Chris", company: "Spectric Labs", "contact": "123-4567", "country": "US" },
48
- { name: "Michael", company: "Spectric Labs", "contact": "123-4567", "country": "US" },
49
- { name: "Matt", company: "Spectric Labs", "contact": "123-4567", "country": "US" },
50
- { name: "Grant", company: "Spectric Labs", "contact": "123-4567", "country": "UK" },]
51
- export const tablecolumns: ColumnSettings<TestData>[] = [{ "title": "Company", key: "company" }, { "title": "Name", key: "name" }, { "title": "Contact", key: "contact" }, { "title": "Country", key: "country", filterable: true }]
47
+ { name: "Sean", company: "Spectric Labs", "contact": "123-4567", location: { "country": "US", state: "VA" }, years: 11 },
48
+ { name: "Kipp", company: "Spectric Labs", "contact": "123-4567", location: { "country": "UK", state: "N/A" }, years: 5 },
49
+ { name: "Adam", company: "Spectric Labs", "contact": "123-4567", location: { "country": "US", state: "VA" }, years: 19 },
50
+ { name: "Chris", company: "Spectric Labs", "contact": "123-4567", location: { "country": "US", state: "VA" }, years: 27 },
51
+ { name: "Michael", company: "Spectric Labs", "contact": "123-4567", location: { "country": "US", state: "VA" }, years: 3 },
52
+ { name: "Matt", company: "Spectric Labs", "contact": "123-4567", location: { "country": "US", state: "VA" }, years: 9 },
53
+ { name: "Matt", company: "Spectric Labs", "contact": "865-6343", location: { "country": "UK", state: "VA" }, years: 5 },
54
+ { name: "Matt", company: "Spectric Labs", "contact": "253-6795", location: { "country": "UK", state: "VA" }, years: 15 },
55
+ { name: "Matt", company: "Spectric Labs (Intern)", "contact": "253-6795", location: { "country": "US", state: "CO" }, years: 1 },
56
+ { name: "Matt", company: "Spectric Labs", "contact": "912-1230", location: { "country": "UK", state: "VA" }, years: 24 },
57
+ { name: "Grant", company: "Spectric Labs", "contact": "123-4567", location: { "country": "US", state: "VA" }, years: 100 },]
58
+ export const tablecolumns: ColumnSettings<TestData>[] = [
59
+ { "title": "Company", key: "company" },
60
+ { "title": "Name", key: "name", sortable: true },
61
+ { "title": "Contact", key: "contact" },
62
+ { "title": "Country", key: "location.country", filterable: true },
63
+ { "title": "Years Employed", key: "years", sortable: true }
64
+ ]
@@ -1,21 +1,16 @@
1
1
  import type { Meta, StoryObj } from "@storybook/web-components";
2
2
 
3
- import { ColumnSettings, PaginationChangeProps, type TableProps as Props } from "../components/";
3
+ import { ColumnSettings, PaginationChangeProps, TableElement, TableSelectOptions, TableSortDirection, TableSortOption, type TableProps as Props } from "../components/";
4
4
  import { html } from "lit";
5
5
  import '../components';
6
6
  import { ifDefined } from "lit/directives/if-defined.js";
7
7
  import { useArgs } from "@storybook/client-api";
8
- import { FilterEvent } from "../components/table/cell";
8
+ import { FilterEvent, rowGetValue } from "../components/table/cell";
9
9
  import { tablecolumns, tabledata } from "./fixtures/data";
10
10
  // More on how to set up stories at: https://storybook.js.org/docs/writing-stories
11
- type TestData = {
12
- name: string
13
- company: string
14
- contact: string
15
- country: string
16
- }
17
- const data = tabledata
11
+ const data = JSON.parse(JSON.stringify(tabledata))
18
12
  const columns = tablecolumns
13
+ const getData = TableElement.getDefaultDataSorterAndPaginatior<typeof tabledata[0]>(data)
19
14
  const meta = {
20
15
  title: "UI/Table",
21
16
  tags: ["autodocs"],
@@ -25,22 +20,29 @@ const meta = {
25
20
 
26
21
  return html`
27
22
  <spectric-table
23
+ style="max-height: 150px;"
28
24
  .pagination=${args.pagination}
29
25
  .columns=${args.columns}
30
- .data=${!args.pagination ? data : data.slice((args.pagination.page - 1) * args.pagination.pageSize, (args.pagination.page) * args.pagination.pageSize)}
26
+ .data=${getData(args)}
31
27
  select=${ifDefined(args.select)}
28
+ sort=${ifDefined(args.sort)}
32
29
  @filter=${(e: CustomEvent<FilterEvent<any>>) => {
33
30
  alert(`filter ${e.detail.include ? "for" : "out"} event value ${e.detail.value}`)
34
31
  }}
35
32
  @change=${(e: CustomEvent<PaginationChangeProps>) => {
36
33
  console.log(e)
37
34
  updateArgs({ ...e.detail });
35
+ if (e.target && e.target instanceof TableElement) {
36
+ e.target.data = getData({ ...args, ...e.detail })
37
+ }
38
38
  }}
39
39
  >
40
40
  </spectric-table>
41
41
  `;
42
42
  },
43
43
  argTypes: {
44
+ select: { control: { type: "select" }, options: Object.values(TableSelectOptions) },
45
+ sort: { control: { type: "select" }, options: Object.values(TableSortOption) }
44
46
  },
45
47
  args: {
46
48
  pagination: {
@@ -49,12 +51,13 @@ const meta = {
49
51
  size: "xsmall",
50
52
  totalItems: data.length,
51
53
  },
52
- columns: columns
54
+ columns: columns,
55
+ select: TableSelectOptions.none
53
56
  },
54
- } satisfies Meta<Props<TestData>>;
57
+ } satisfies Meta<Props<typeof tabledata[0]>>;
55
58
 
56
59
  export default meta;
57
- type Story = StoryObj<Props<TestData>>;
60
+ type Story = StoryObj<Props<typeof tabledata[0]>>;
58
61
 
59
62
  // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
60
63
  export const Basic: Story = {
@@ -85,4 +88,15 @@ export const SingleSelect: Story = {
85
88
  args: {
86
89
  select: "single"
87
90
  },
91
+ };
92
+
93
+
94
+ /**
95
+ * The spectric table by default is set to single column sorting, but you can enable multicolumn sorting with the sort="multi" attribute
96
+ */
97
+ export const MultiColumnSort: Story = {
98
+ args: {
99
+ sort: "multi",
100
+ pagination: { pageSize: 20, page: 1 }
101
+ },
88
102
  };
@@ -0,0 +1,12 @@
1
+ export function once(func: Function) {
2
+ let hasBeenCalled = false;
3
+ let result: any;
4
+ return function (...args: any[]) {
5
+ if (!hasBeenCalled) {
6
+ hasBeenCalled = true;
7
+ //@ts-ignore
8
+ result = func.apply(this, args);
9
+ }
10
+ return result;
11
+ };
12
+ }
@@ -7,7 +7,7 @@ import { AsyncDirective, directive } from 'lit/async-directive.js';
7
7
  /**
8
8
  * Usage:
9
9
  * import { html, render } from 'lit';
10
- * import { spreadProps } from '@open-wc/lit-helpers';
10
+ * import { spreadProps } from '@spectric/ui';
11
11
  *
12
12
  * render(
13
13
  * html`
@@ -44,10 +44,10 @@ export class SpreadPropsDirective extends AsyncDirective {
44
44
 
45
45
  apply(data: any) {
46
46
  if (!data) return;
47
- const { prevData, element } = this;
47
+ const { element } = this;
48
48
  for (const key in data) {
49
49
  const value = data[key];
50
- if (value === prevData[key]) {
50
+ if ((element as any)[key] === value) {
51
51
  continue;
52
52
  }
53
53
  // @ts-ignore