asljs-ui-list 0.0.0-stage → 0.1.1
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.
- package/LICENSE.md +51 -0
- package/README.md +81 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/list.d.ts +26 -0
- package/dist/list.js +313 -0
- package/dist/theme.d.ts +13 -0
- package/dist/theme.js +4 -0
- package/docs/List.md +106 -0
- package/package.json +63 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Permissive No-Derivatives License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandrite Software Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software (the "Software") to:
|
|
7
|
+
|
|
8
|
+
1. Use the Software for any purpose, including commercial purposes.
|
|
9
|
+
2. Copy and redistribute the Software, including as part of another application,
|
|
10
|
+
library, or package.
|
|
11
|
+
3. Modify the Software for private or internal use.
|
|
12
|
+
|
|
13
|
+
Subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
## 1. No Distribution of Modified Versions
|
|
16
|
+
|
|
17
|
+
Modified versions of the Software, or substantial portions thereof, must not be
|
|
18
|
+
distributed without prior written permission from the copyright holder.
|
|
19
|
+
|
|
20
|
+
Routine build transformations, including minification, transpilation, and
|
|
21
|
+
bundling, are permitted for distribution, provided they do not intentionally
|
|
22
|
+
alter the Software's functionality.
|
|
23
|
+
|
|
24
|
+
## 2. Licence and Copyright Preservation
|
|
25
|
+
|
|
26
|
+
All distributed copies of the Software must retain this copyright notice and
|
|
27
|
+
licence.
|
|
28
|
+
|
|
29
|
+
The Software itself, including copies incorporated into other applications,
|
|
30
|
+
libraries, or packages, remains subject to this licence.
|
|
31
|
+
|
|
32
|
+
The Software must not be sublicensed or redistributed under a different licence.
|
|
33
|
+
|
|
34
|
+
## 3. Use in Other Software
|
|
35
|
+
|
|
36
|
+
Applications and libraries that merely use, import, link to, or bundle the
|
|
37
|
+
Software are not considered modified versions of the Software and may be
|
|
38
|
+
distributed under any licence.
|
|
39
|
+
|
|
40
|
+
Such use, incorporation, or bundling does not grant permission to relicense the
|
|
41
|
+
Software or distribute modified versions of it.
|
|
42
|
+
|
|
43
|
+
### 4. No Warranty
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
47
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
48
|
+
|
|
49
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
50
|
+
OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS
|
|
51
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# list
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Part of [Alexandrite Software Library][#1] - a set of high-quality, performant
|
|
4
|
+
> JavaScript libraries and tools for everyday use.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`asljs-ui-list` provides `asljs-list`, which renders a collection from
|
|
9
|
+
templates. Each row is bound with `asljs-data-binding` to its own context: the
|
|
10
|
+
item, its position, and the shared `context` of row actions.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
- Items as a plain array or an eventful-like collection the list re-renders from
|
|
15
|
+
when it changes.
|
|
16
|
+
- Row, empty-state and container templates, local or from the `list` theme
|
|
17
|
+
section; `createBootstrapListTheme()` supplies a Bootstrap list group.
|
|
18
|
+
- Render callbacks and inline call expressions are outside the model: rows come
|
|
19
|
+
from templates and handlers are binding paths.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install asljs-ui-list
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
NPM package: [asljs-ui-list][NPM]
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Import the package once to register the element:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import 'asljs-ui-list';
|
|
35
|
+
|
|
36
|
+
const list =
|
|
37
|
+
document.createElement('asljs-list');
|
|
38
|
+
|
|
39
|
+
list.context = {
|
|
40
|
+
select(this: { item: { title: string } }) {
|
|
41
|
+
console.log('selected', this.item.title);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
list.innerHTML = `
|
|
46
|
+
<template data-slot="item">
|
|
47
|
+
<a data-bind-text="item.title"
|
|
48
|
+
data-bind-on-click="context.select"></a>
|
|
49
|
+
</template>
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
list.items =
|
|
53
|
+
[ { title: 'First' },
|
|
54
|
+
{ title: 'Second' } ];
|
|
55
|
+
|
|
56
|
+
document.body.appendChild(list);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Further reading
|
|
60
|
+
|
|
61
|
+
- [List][DOC] - templates, the row binding context and row actions.
|
|
62
|
+
- [Theming][THM] - templates, themes and how they combine.
|
|
63
|
+
|
|
64
|
+
Questions and bugs: [asljs/issues][ISS].
|
|
65
|
+
|
|
66
|
+
## Related packages
|
|
67
|
+
|
|
68
|
+
- [ui][UI] - the shared theme and the other UI packages.
|
|
69
|
+
- `asljs-data-binding` for the bindings the row templates use.
|
|
70
|
+
- `asljs-eventful` for the collections the list follows.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT License. See [LICENSE][LIC] for details.
|
|
75
|
+
|
|
76
|
+
[#1]: https://github.com/AlexandriteSoftware/asljs
|
|
77
|
+
[DOC]: docs/List.md
|
|
78
|
+
[ISS]: https://github.com/AlexandriteSoftware/asljs/issues
|
|
79
|
+
[LIC]: LICENSE.md
|
|
80
|
+
[NPM]: https://www.npmjs.com/package/asljs-ui-list
|
|
81
|
+
[THM]: ../core/docs/Theming.md
|
|
82
|
+
[UI]: ../core/README.md
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/list.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ComponentModelDefinition, ComponentsTheme } from 'asljs-ui';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
export type ListRowContext = {
|
|
4
|
+
item: ListItem;
|
|
5
|
+
index: number;
|
|
6
|
+
context: unknown;
|
|
7
|
+
first: boolean;
|
|
8
|
+
last: boolean;
|
|
9
|
+
odd: boolean;
|
|
10
|
+
even: boolean;
|
|
11
|
+
count: number;
|
|
12
|
+
};
|
|
13
|
+
export type ListItem = Record<string, unknown>;
|
|
14
|
+
export type ListItemsSource = ListItem[];
|
|
15
|
+
export declare const ListModelDefinition: ComponentModelDefinition;
|
|
16
|
+
export declare class List extends LitElement {
|
|
17
|
+
#private;
|
|
18
|
+
accessor items: ListItemsSource;
|
|
19
|
+
accessor context: unknown;
|
|
20
|
+
accessor theme: ComponentsTheme | null;
|
|
21
|
+
createRenderRoot(): this;
|
|
22
|
+
connectedCallback(): void;
|
|
23
|
+
disconnectedCallback(): void;
|
|
24
|
+
render(): ReturnType<LitElement['render']>;
|
|
25
|
+
updated(changedProperties: Map<PropertyKey, unknown>): void;
|
|
26
|
+
}
|
package/dist/list.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { bindDataModel } from 'asljs-data-binding';
|
|
8
|
+
import { asEventfulLike } from 'asljs-eventful';
|
|
9
|
+
import { defineComponentModel, findThemeProvider, getDefaultTheme, resolveThemeTemplate, THEME_CHANGED_EVENT_NAME } from 'asljs-ui';
|
|
10
|
+
import { html, LitElement, nothing } from 'lit';
|
|
11
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
12
|
+
export const ListModelDefinition = { name: 'ListModelDefinition',
|
|
13
|
+
title: 'List',
|
|
14
|
+
properties: [{ name: 'items',
|
|
15
|
+
title: 'Items',
|
|
16
|
+
type: 'array',
|
|
17
|
+
description: 'Rows rendered by the list.' },
|
|
18
|
+
{ name: 'context',
|
|
19
|
+
title: 'Context',
|
|
20
|
+
type: 'object',
|
|
21
|
+
description: 'Shared row binding context.' },
|
|
22
|
+
{ name: 'theme',
|
|
23
|
+
title: 'Theme',
|
|
24
|
+
type: 'object',
|
|
25
|
+
description: 'Per-instance components theme override.' }] };
|
|
26
|
+
defineComponentModel('asljs-list', ListModelDefinition);
|
|
27
|
+
let List = class List extends LitElement {
|
|
28
|
+
#containerTemplate = null;
|
|
29
|
+
#emptyTemplate = null;
|
|
30
|
+
#itemTemplate = null;
|
|
31
|
+
#itemBindingDisposers = [];
|
|
32
|
+
#itemsObserverDispose = null;
|
|
33
|
+
#themeProvider = null;
|
|
34
|
+
#warnedMissingItemTemplate = false;
|
|
35
|
+
#warnedInvalidContainer = false;
|
|
36
|
+
#handleThemeChanged = () => {
|
|
37
|
+
this.requestUpdate();
|
|
38
|
+
};
|
|
39
|
+
#items_accessor_storage = [];
|
|
40
|
+
get items() { return this.#items_accessor_storage; }
|
|
41
|
+
set items(value) { this.#items_accessor_storage = value; }
|
|
42
|
+
#context_accessor_storage = undefined;
|
|
43
|
+
get context() { return this.#context_accessor_storage; }
|
|
44
|
+
set context(value) { this.#context_accessor_storage = value; }
|
|
45
|
+
#theme_accessor_storage = null;
|
|
46
|
+
get theme() { return this.#theme_accessor_storage; }
|
|
47
|
+
set theme(value) { this.#theme_accessor_storage = value; }
|
|
48
|
+
createRenderRoot() {
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
connectedCallback() {
|
|
52
|
+
super.connectedCallback();
|
|
53
|
+
this.#captureTemplates();
|
|
54
|
+
this.#syncItemsObserver();
|
|
55
|
+
this.#syncThemeProvider();
|
|
56
|
+
}
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this.#disposeItemsObserver();
|
|
59
|
+
this.#disposeItemBindings();
|
|
60
|
+
this.#disposeThemeProvider();
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
}
|
|
63
|
+
render() {
|
|
64
|
+
const emptyTemplate = this.#resolveTemplate('empty');
|
|
65
|
+
const itemTemplate = this.#resolveTemplate('item');
|
|
66
|
+
const containerTemplate = this.#resolveTemplate('container');
|
|
67
|
+
if (this.items.length === 0) {
|
|
68
|
+
if (emptyTemplate) {
|
|
69
|
+
return html `
|
|
70
|
+
<div data-role="empty-template-host"></div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
return nothing;
|
|
74
|
+
}
|
|
75
|
+
if (!itemTemplate) {
|
|
76
|
+
return nothing;
|
|
77
|
+
}
|
|
78
|
+
if (containerTemplate) {
|
|
79
|
+
return html `
|
|
80
|
+
<div data-role="container-template-host"></div>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
return html `
|
|
84
|
+
<div data-role="default-container-host">
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
updated(changedProperties) {
|
|
89
|
+
if (changedProperties.has('items')) {
|
|
90
|
+
this.#syncItemsObserver();
|
|
91
|
+
}
|
|
92
|
+
if (changedProperties.has('theme')) {
|
|
93
|
+
this.#syncThemeProvider();
|
|
94
|
+
}
|
|
95
|
+
this.#renderTemplateContent();
|
|
96
|
+
}
|
|
97
|
+
#captureTemplates() {
|
|
98
|
+
this.#containerTemplate = null;
|
|
99
|
+
this.#emptyTemplate = null;
|
|
100
|
+
this.#itemTemplate = null;
|
|
101
|
+
this.#warnedMissingItemTemplate = false;
|
|
102
|
+
this.#warnedInvalidContainer = false;
|
|
103
|
+
const templateElements = this.querySelectorAll('template[data-slot]');
|
|
104
|
+
for (const item of templateElements) {
|
|
105
|
+
const templateElement = item;
|
|
106
|
+
const slotName = templateElement.getAttribute('data-slot');
|
|
107
|
+
const clonedTemplate = document.createElement('template');
|
|
108
|
+
clonedTemplate.content.append(templateElement.content.cloneNode(true));
|
|
109
|
+
if (slotName === 'empty') {
|
|
110
|
+
this.#emptyTemplate = clonedTemplate;
|
|
111
|
+
}
|
|
112
|
+
if (slotName === 'item') {
|
|
113
|
+
this.#itemTemplate = clonedTemplate;
|
|
114
|
+
}
|
|
115
|
+
if (slotName === 'container') {
|
|
116
|
+
this.#containerTemplate = clonedTemplate;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
#renderTemplateContent() {
|
|
121
|
+
this.#renderEmptyTemplate();
|
|
122
|
+
this.#renderItemTemplates();
|
|
123
|
+
}
|
|
124
|
+
#renderEmptyTemplate() {
|
|
125
|
+
const emptyTemplate = this.#resolveTemplate('empty');
|
|
126
|
+
if (!emptyTemplate
|
|
127
|
+
|| this.items.length > 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const host = this.querySelector('[data-role="empty-template-host"]');
|
|
131
|
+
if (!host) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
host.replaceChildren(emptyTemplate.content.cloneNode(true));
|
|
135
|
+
}
|
|
136
|
+
#renderItemTemplates() {
|
|
137
|
+
this.#disposeItemBindings();
|
|
138
|
+
const itemTemplate = this.#resolveTemplate('item');
|
|
139
|
+
if (!itemTemplate
|
|
140
|
+
|| this.items.length === 0) {
|
|
141
|
+
if (!itemTemplate
|
|
142
|
+
&& this.items.length > 0) {
|
|
143
|
+
this.#warnMissingItemTemplate();
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const itemsHost = this.#resolveItemsHost();
|
|
148
|
+
if (!itemsHost) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const rowNodes = [];
|
|
152
|
+
const count = this.items.length;
|
|
153
|
+
for (let index = 0; index < this.items.length; index++) {
|
|
154
|
+
const item = this.items[index];
|
|
155
|
+
const rowContext = { item,
|
|
156
|
+
index,
|
|
157
|
+
context: this.#createRowScopeContext(item, index),
|
|
158
|
+
first: index === 0,
|
|
159
|
+
last: index === count - 1,
|
|
160
|
+
odd: index % 2 === 1,
|
|
161
|
+
even: index % 2 === 0,
|
|
162
|
+
count };
|
|
163
|
+
const fragment = itemTemplate.content.cloneNode(true);
|
|
164
|
+
this.#bindFragmentModel(fragment, rowContext);
|
|
165
|
+
rowNodes.push(...[...fragment.childNodes]);
|
|
166
|
+
}
|
|
167
|
+
itemsHost.replaceChildren(...rowNodes);
|
|
168
|
+
}
|
|
169
|
+
#resolveItemsHost() {
|
|
170
|
+
const containerTemplate = this.#resolveTemplate('container');
|
|
171
|
+
if (containerTemplate) {
|
|
172
|
+
const containerHost = this.querySelector('[data-role="container-template-host"]');
|
|
173
|
+
if (!containerHost) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const fragment = containerTemplate.content.cloneNode(true);
|
|
177
|
+
const templatedItemsHost = fragment.querySelector('[data-role="items"]');
|
|
178
|
+
containerHost.replaceChildren(fragment);
|
|
179
|
+
if (!templatedItemsHost) {
|
|
180
|
+
this.#warnInvalidContainer();
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return templatedItemsHost;
|
|
184
|
+
}
|
|
185
|
+
const defaultContainerHost = this.querySelector('[data-role="default-container-host"]');
|
|
186
|
+
return defaultContainerHost;
|
|
187
|
+
}
|
|
188
|
+
#resolveTemplate(slotName) {
|
|
189
|
+
return this.#getLocalTemplate(slotName)
|
|
190
|
+
?? this.#getThemeTemplate(slotName);
|
|
191
|
+
}
|
|
192
|
+
#getLocalTemplate(slotName) {
|
|
193
|
+
if (slotName === 'container') {
|
|
194
|
+
return this.#containerTemplate;
|
|
195
|
+
}
|
|
196
|
+
if (slotName === 'empty') {
|
|
197
|
+
return this.#emptyTemplate;
|
|
198
|
+
}
|
|
199
|
+
return this.#itemTemplate;
|
|
200
|
+
}
|
|
201
|
+
#getThemeTemplate(slotName) {
|
|
202
|
+
const activeTheme = this.theme
|
|
203
|
+
?? this.#themeProvider?.theme
|
|
204
|
+
?? getDefaultTheme();
|
|
205
|
+
return resolveThemeTemplate(activeTheme.list?.[slotName], this);
|
|
206
|
+
}
|
|
207
|
+
#bindFragmentModel(fragment, model) {
|
|
208
|
+
const dispose = bindDataModel(fragment, model);
|
|
209
|
+
this.#itemBindingDisposers.push(dispose);
|
|
210
|
+
}
|
|
211
|
+
#createRowScopeContext(item, index) {
|
|
212
|
+
if (this.context === null
|
|
213
|
+
|| this.context === undefined
|
|
214
|
+
|| typeof this.context
|
|
215
|
+
!== 'object') {
|
|
216
|
+
return this.context;
|
|
217
|
+
}
|
|
218
|
+
const baseContext = this.context;
|
|
219
|
+
const rowContext = Object.create(baseContext);
|
|
220
|
+
rowContext.item = item;
|
|
221
|
+
rowContext.index = index;
|
|
222
|
+
for (const key of Object.keys(baseContext)) {
|
|
223
|
+
const value = baseContext[key];
|
|
224
|
+
if (typeof value
|
|
225
|
+
=== 'function') {
|
|
226
|
+
rowContext[key] =
|
|
227
|
+
value.bind(rowContext);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return rowContext;
|
|
231
|
+
}
|
|
232
|
+
#disposeItemBindings() {
|
|
233
|
+
for (const dispose of this.#itemBindingDisposers) {
|
|
234
|
+
dispose();
|
|
235
|
+
}
|
|
236
|
+
this.#itemBindingDisposers = [];
|
|
237
|
+
}
|
|
238
|
+
#syncItemsObserver() {
|
|
239
|
+
this.#disposeItemsObserver();
|
|
240
|
+
const eventSource = toEventfulLike(this.items);
|
|
241
|
+
if (!eventSource) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const unsubscribers = [];
|
|
245
|
+
const onCollectionChanged = () => {
|
|
246
|
+
this.requestUpdate();
|
|
247
|
+
};
|
|
248
|
+
// An observable collection reports every change as one `change` event,
|
|
249
|
+
// carrying the list of what changed; the list re-reads it all anyway.
|
|
250
|
+
const unsubscribe = eventSource.on('change', onCollectionChanged);
|
|
251
|
+
unsubscribers.push(() => {
|
|
252
|
+
unsubscribe();
|
|
253
|
+
});
|
|
254
|
+
this.#itemsObserverDispose =
|
|
255
|
+
() => {
|
|
256
|
+
for (const unsubscribe of unsubscribers) {
|
|
257
|
+
unsubscribe();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
#disposeItemsObserver() {
|
|
262
|
+
if (this.#itemsObserverDispose) {
|
|
263
|
+
this.#itemsObserverDispose();
|
|
264
|
+
this.#itemsObserverDispose = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
#syncThemeProvider() {
|
|
268
|
+
const nextProvider = this.theme
|
|
269
|
+
? null
|
|
270
|
+
: findThemeProvider(this);
|
|
271
|
+
if (this.#themeProvider === nextProvider) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.#disposeThemeProvider();
|
|
275
|
+
this.#themeProvider = nextProvider;
|
|
276
|
+
this.#themeProvider?.addEventListener(THEME_CHANGED_EVENT_NAME, this.#handleThemeChanged);
|
|
277
|
+
}
|
|
278
|
+
#disposeThemeProvider() {
|
|
279
|
+
this.#themeProvider?.removeEventListener(THEME_CHANGED_EVENT_NAME, this.#handleThemeChanged);
|
|
280
|
+
this.#themeProvider = null;
|
|
281
|
+
}
|
|
282
|
+
#warnMissingItemTemplate() {
|
|
283
|
+
if (this.#warnedMissingItemTemplate) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.#warnedMissingItemTemplate = true;
|
|
287
|
+
console.warn('asljs-list: missing required template[data-slot="item"] for non-empty items.');
|
|
288
|
+
}
|
|
289
|
+
#warnInvalidContainer() {
|
|
290
|
+
if (this.#warnedInvalidContainer) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.#warnedInvalidContainer = true;
|
|
294
|
+
console.warn('asljs-list: container template must include [data-role="items"].');
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
__decorate([
|
|
298
|
+
property({ attribute: false })
|
|
299
|
+
], List.prototype, "items", null);
|
|
300
|
+
__decorate([
|
|
301
|
+
property({ attribute: false })
|
|
302
|
+
], List.prototype, "context", null);
|
|
303
|
+
__decorate([
|
|
304
|
+
property({ attribute: false })
|
|
305
|
+
], List.prototype, "theme", null);
|
|
306
|
+
List = __decorate([
|
|
307
|
+
customElement('asljs-list')
|
|
308
|
+
], List);
|
|
309
|
+
export { List };
|
|
310
|
+
function toEventfulLike(value) {
|
|
311
|
+
return asEventfulLike(value)
|
|
312
|
+
?? null;
|
|
313
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ComponentsTheme, type ThemeTemplateValue } from 'asljs-ui';
|
|
2
|
+
export interface ListThemeDefinition {
|
|
3
|
+
container?: ThemeTemplateValue;
|
|
4
|
+
empty?: ThemeTemplateValue;
|
|
5
|
+
item?: ThemeTemplateValue;
|
|
6
|
+
}
|
|
7
|
+
declare module 'asljs-ui' {
|
|
8
|
+
interface ComponentsTheme {
|
|
9
|
+
list?: ListThemeDefinition;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The Bootstrap list section. */
|
|
13
|
+
export declare function createBootstrapListTheme(): ComponentsTheme;
|
package/dist/theme.js
ADDED
package/docs/List.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# List
|
|
2
|
+
|
|
3
|
+
`asljs-list` renders a collection from templates, binding each row to its own
|
|
4
|
+
context.
|
|
5
|
+
|
|
6
|
+
## Properties
|
|
7
|
+
|
|
8
|
+
- `items` — a plain array, or an eventful-like collection the list re-renders
|
|
9
|
+
from when it changes
|
|
10
|
+
- `context` — shared row actions and state
|
|
11
|
+
- `theme`
|
|
12
|
+
- local slot templates
|
|
13
|
+
|
|
14
|
+
## Templates
|
|
15
|
+
|
|
16
|
+
- `template[data-slot="item"]` — required; renders each row
|
|
17
|
+
- `template[data-slot="empty"]` — optional; the empty state
|
|
18
|
+
- `template[data-slot="container"]` — optional; must contain a
|
|
19
|
+
`[data-role="items"]` insertion point
|
|
20
|
+
|
|
21
|
+
If `items` is non-empty and no item template is provided, the component warns
|
|
22
|
+
and renders nothing. Themes can supply fallback templates; local templates
|
|
23
|
+
always win. The theme section is `list`; `createBootstrapListTheme()` supplies
|
|
24
|
+
Bootstrap list-group markup. See [Theming][THM].
|
|
25
|
+
|
|
26
|
+
## Row binding context
|
|
27
|
+
|
|
28
|
+
Each row is bound with `asljs-data-binding` to a row context with these fields:
|
|
29
|
+
|
|
30
|
+
- `item`: the current row record
|
|
31
|
+
- `index`: the zero-based row index
|
|
32
|
+
- `first`: `true` for the first row
|
|
33
|
+
- `last`: `true` for the last row
|
|
34
|
+
- `odd`: `true` for odd row positions
|
|
35
|
+
- `even`: `true` for even row positions
|
|
36
|
+
- `count`: the total item count
|
|
37
|
+
- `context`: the shared base context, adapted to the current row
|
|
38
|
+
|
|
39
|
+
## Row actions
|
|
40
|
+
|
|
41
|
+
- Reference shared actions through the row context, for example
|
|
42
|
+
`data-bind-on-click="context.select"`.
|
|
43
|
+
- Row-specific values arrive through the derived row-local `this`, which
|
|
44
|
+
includes row fields such as `item` and `index`.
|
|
45
|
+
- Arguments are not written in attributes: `select(item.id)` is not a binding.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import 'asljs-ui-list';
|
|
49
|
+
|
|
50
|
+
const list =
|
|
51
|
+
document.createElement('asljs-list');
|
|
52
|
+
|
|
53
|
+
list.context = {
|
|
54
|
+
select(this: { item: { id: string; title: string } }, event: Event) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
console.log('selected', this.item.id, this.item.title);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
list.innerHTML = `
|
|
61
|
+
<template data-slot="container">
|
|
62
|
+
<section class="rows" data-role="items"></section>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<template data-slot="empty">
|
|
66
|
+
<div>No items</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<template data-slot="item">
|
|
70
|
+
<div>
|
|
71
|
+
<a data-bind-href="item.url"
|
|
72
|
+
data-bind-text="item.title"
|
|
73
|
+
data-bind-on-click="context.select"></a>
|
|
74
|
+
<small data-bind-text="index"></small>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
list.items =
|
|
80
|
+
[ { title: 'First', url: '/first' },
|
|
81
|
+
{ title: 'Second', url: '/second' } ];
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Authoring rules
|
|
85
|
+
|
|
86
|
+
Rendering is template-driven. That shapes how a list is written:
|
|
87
|
+
|
|
88
|
+
- Rows come from templates, not from React-style render callbacks or prop-driven
|
|
89
|
+
row renderers.
|
|
90
|
+
- Bindings are `asljs-data-binding` paths, not template expressions with inline
|
|
91
|
+
function calls, and event handlers follow its path-based rules.
|
|
92
|
+
- Only the documented slots are used, and a container template must keep its
|
|
93
|
+
`[data-role="items"]` insertion point.
|
|
94
|
+
- Keep row templates declarative, and use `context` methods for shared row
|
|
95
|
+
actions rather than custom attribute protocols.
|
|
96
|
+
- Do not mutate slot templates at runtime. Update `list.items`, or the source
|
|
97
|
+
collection, instead of rewriting row DOM.
|
|
98
|
+
|
|
99
|
+
## Exports
|
|
100
|
+
|
|
101
|
+
- `List` and `ListModelDefinition`
|
|
102
|
+
- `ListItem`, `ListItemsSource` and `ListRowContext` types
|
|
103
|
+
- `createBootstrapListTheme()` and the `ListThemeDefinition` type; the shared
|
|
104
|
+
theming exports of `asljs-ui` are listed in [Theming][THM]
|
|
105
|
+
|
|
106
|
+
[THM]: ../../core/docs/Theming.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-ui-list",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A web component that renders a collection from templates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"asljs": {
|
|
7
|
+
"type": "lib"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE.md",
|
|
21
|
+
"docs/**"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"flint": "toolkit flint",
|
|
25
|
+
"clean": "toolkit clean",
|
|
26
|
+
"build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
|
|
27
|
+
"build:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
|
|
28
|
+
"typecheck": "npm run codegen --if-present && tsc -p tsconfig.build.json --noEmit",
|
|
29
|
+
"guard:clean-git": "toolkit ensure-clean-working-directory",
|
|
30
|
+
"prepack": "npm run clean && npm run build:dist && npm run build && npm run test && npm run guard:clean-git",
|
|
31
|
+
"postpublish": "toolkit tag-release-revision",
|
|
32
|
+
"test": "node --test --enable-source-maps \"build/**/*.test.js\"",
|
|
33
|
+
"test:watch": "node --watch --test \"build/**/*.test.js\"",
|
|
34
|
+
"coverage": "npm run build && node --test --experimental-test-coverage --enable-source-maps --test-coverage-exclude=\"**/*.test.js\" \"build/**/*.test.js\"",
|
|
35
|
+
"release:patch": "toolkit release-patch",
|
|
36
|
+
"all": "npm run clean && npm run build:dist && npm run flint && npm run typecheck && npm run build && npm run test"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"web-components",
|
|
40
|
+
"lit",
|
|
41
|
+
"ui",
|
|
42
|
+
"list"
|
|
43
|
+
],
|
|
44
|
+
"author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
|
|
45
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
46
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"asljs-data-binding": "^0.2.5",
|
|
56
|
+
"asljs-eventful": "^0.4.11",
|
|
57
|
+
"asljs-ui": "^0.1.1",
|
|
58
|
+
"lit": "^3.3.1"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"asljs-observable": "^0.5.5",
|
|
62
|
+
"asljs-testing": "^0.1.1",
|
|
63
|
+
"asljs-toolkit": "^0.1.0"
|
|
64
|
+
}
|
|
65
|
+
}
|