@vanilla-bean/components 1.0.0
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/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- package/utils/string.js +72 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Router
|
|
2
|
+
|
|
3
|
+
Hash-based client-side router that maps URL fragments to component classes. Each route pattern maps to a view class; the Router instantiates the matching view and destroys the previous one on navigation.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Router, View, Page } from '@vanilla-bean/components';
|
|
9
|
+
|
|
10
|
+
class HomeView extends View {
|
|
11
|
+
build() {
|
|
12
|
+
// render view content here — no Page needed, the shell provides it
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class UserView extends View {
|
|
17
|
+
build() {
|
|
18
|
+
// route parameters from /users/:id are passed as options
|
|
19
|
+
console.log(this.options.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class NotFoundView extends View {
|
|
24
|
+
build() {
|
|
25
|
+
// 404 content
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const page = new Page({ title: 'My App', appendTo: document.body });
|
|
30
|
+
|
|
31
|
+
new Router({
|
|
32
|
+
views: {
|
|
33
|
+
'/': HomeView,
|
|
34
|
+
'/users/:id': UserView,
|
|
35
|
+
},
|
|
36
|
+
defaultPath: '/',
|
|
37
|
+
notFound: NotFoundView,
|
|
38
|
+
appendTo: page,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
| Option | Type | Default | Description |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| `views` | `object` | — | **Required.** Maps route patterns to component classes. Patterns may contain `:param` segments. |
|
|
47
|
+
| `defaultPath` | `string` | First key of `views` | Route used when the hash is empty and for fallback on unmatched routes. |
|
|
48
|
+
| `notFound` | `typeof Component` | — | Component class rendered when no route matches and `defaultPath` fallback also fails. Receives `{ route }` as an option. |
|
|
49
|
+
| `onRenderView` | `Function` | — | Called with the matched route string whenever a new view is rendered. |
|
|
50
|
+
| `mode` | `'hash' \| 'history'` | `'hash'` | `'hash'` uses URL fragments (`#/path`). `'history'` uses `pushState` and real pathnames (`/path`). History mode requires the server to serve `index.html` for all routes. |
|
|
51
|
+
|
|
52
|
+
## Properties
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
router.path; // string — current URL hash, stripped of '#/', '/', and query strings
|
|
56
|
+
router.path = '/users/42'; // navigate; sets window.location.hash and re-renders
|
|
57
|
+
|
|
58
|
+
router.route; // string — the matched route pattern for the current path (e.g. '/users/:id')
|
|
59
|
+
router.currentRoute; // string — the last successfully rendered route pattern
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Methods
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
router.parseRouteParameters(path?: string): object
|
|
66
|
+
// Extracts named :param segments from a path.
|
|
67
|
+
// Defaults to the current path if none is provided.
|
|
68
|
+
// '/users/42' against '/users/:id' → { id: '42' }
|
|
69
|
+
|
|
70
|
+
router.pathToRoute(path: string): string
|
|
71
|
+
// Returns the matching route pattern for a path, or the path itself if no pattern matches.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Route patterns
|
|
75
|
+
|
|
76
|
+
Routes are matched using exact string equality first, then by replacing `:param` segments with a capture regex. When multiple parameterized patterns could match, routes with fewer `:param` segments are tested first, more specific patterns win without relying on object key order.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
views: {
|
|
80
|
+
'/users/new': NewUserView, // exact match — always wins
|
|
81
|
+
'/users/:id': UserView, // one param — tested before /:section/:id
|
|
82
|
+
'/:section/:id': SectionView, // two params — tested last
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Query strings are stripped before matching: `#/users/42?sort=name` resolves to `/users/42`.
|
|
87
|
+
|
|
88
|
+
## Route parameters
|
|
89
|
+
|
|
90
|
+
Segments prefixed with `:` in the pattern are extracted and passed as options to the rendered view class:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
views: { '/users/:id': UserView }
|
|
94
|
+
|
|
95
|
+
// Navigating to #/users/42 renders: new UserView({ id: '42', appendTo: this.elem })
|
|
96
|
+
class UserView extends View {
|
|
97
|
+
build() {
|
|
98
|
+
console.log(this.options.id); // '42'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Navigation behavior
|
|
104
|
+
|
|
105
|
+
- **Same-route navigation is a no-op.** Navigating to the already-active route does not destroy and rebuild the view. View state is preserved.
|
|
106
|
+
- **Empty hash** uses `defaultPath`. The hash is set and a re-render is triggered.
|
|
107
|
+
- **Unmatched routes** fall back to `defaultPath` first, then `notFound` if `defaultPath` also fails.
|
|
108
|
+
- The Router listens to both `popstate` and `hashchange`, and the browser back/forward buttons work without any extra setup.
|
|
109
|
+
|
|
110
|
+
## View base class
|
|
111
|
+
|
|
112
|
+
`View` is a styled `Component` exported alongside `Router`. It fills its container (`width: 100%; height: 100%; display: flex; flex-direction: column; flex: 1`) and is intended as the base class for route views, but is not required. Any `Component` subclass works as a view.
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { Router, View } from '@vanilla-bean/components';
|
|
116
|
+
|
|
117
|
+
class SettingsView extends View {
|
|
118
|
+
build() {
|
|
119
|
+
/* ... */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Example — full SPA shell
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { Router, View, Nav, NavItem, Page } from '@vanilla-bean/components';
|
|
128
|
+
|
|
129
|
+
class HomeView extends View {
|
|
130
|
+
build() {
|
|
131
|
+
new Component({ textContent: 'Home content', appendTo: this });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ArticleView extends View {
|
|
136
|
+
build() {
|
|
137
|
+
new Component({ textContent: `Article: ${this.options.slug}`, appendTo: this });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Page is the app shell — Router and Nav live inside it, not inside each view
|
|
142
|
+
const page = new Page({ title: 'My App', appendTo: document.body });
|
|
143
|
+
|
|
144
|
+
const router = new Router({
|
|
145
|
+
views: {
|
|
146
|
+
'/': HomeView,
|
|
147
|
+
'/articles/:slug': ArticleView,
|
|
148
|
+
},
|
|
149
|
+
defaultPath: '/',
|
|
150
|
+
appendTo: page,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
new Nav({
|
|
154
|
+
appendTo: page,
|
|
155
|
+
append: [
|
|
156
|
+
new NavItem({ textContent: 'Home', onPointerPress: () => (router.path = '/') }),
|
|
157
|
+
new NavItem({ textContent: 'Latest', onPointerPress: () => (router.path = '/articles/latest') }),
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Component } from '../../Component';
|
|
2
|
+
import { routeToRegex } from './utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client-side router component with hash-based or history API routing and dynamic view rendering.
|
|
6
|
+
*
|
|
7
|
+
* Provides single-page application routing with support for route parameters,
|
|
8
|
+
* dynamic view rendering, and browser history integration.
|
|
9
|
+
* @param {object} [options={}] - Router configuration options
|
|
10
|
+
* @param {object} options.views - Object mapping route patterns to component classes
|
|
11
|
+
* @param {string} [options.defaultPath] - Default route path, uses first view key if not specified
|
|
12
|
+
* @param {Component} [options.notFound] - Component class for 404/not found routes
|
|
13
|
+
* @param {Function} [options.onRenderView] - Callback fired when rendering a new view
|
|
14
|
+
* @param {'hash'|'history'} [options.mode='hash'] - Routing mode: 'hash' uses URL fragments, 'history' uses pushState
|
|
15
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
16
|
+
* @returns {Router} Router component instance
|
|
17
|
+
*/
|
|
18
|
+
class Router extends Component {
|
|
19
|
+
constructor(options = {}, ...children) {
|
|
20
|
+
super(
|
|
21
|
+
{
|
|
22
|
+
defaultPath: Object.keys(options.views)[0],
|
|
23
|
+
...options,
|
|
24
|
+
style: {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
flex: 1,
|
|
27
|
+
overflow: 'hidden',
|
|
28
|
+
...options.style,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
...children,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the current path from the URL.
|
|
37
|
+
* @returns {string} Current route path
|
|
38
|
+
*/
|
|
39
|
+
get path() {
|
|
40
|
+
if (this.options.mode === 'history') return window.location.pathname;
|
|
41
|
+
|
|
42
|
+
return window.location.hash.replace(/^#\/?/, '/').replace(/\?.*$/, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sets the current path and triggers view rendering.
|
|
47
|
+
* @param {string} path - New route path to navigate to
|
|
48
|
+
*/
|
|
49
|
+
set path(path) {
|
|
50
|
+
if (this.options.mode === 'history') {
|
|
51
|
+
window.history.pushState(null, '', path);
|
|
52
|
+
} else {
|
|
53
|
+
window.location.hash = path;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.renderView();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Gets the matched route for the current path.
|
|
61
|
+
* @returns {string} Matched route pattern
|
|
62
|
+
*/
|
|
63
|
+
get route() {
|
|
64
|
+
return this.pathToRoute(this.path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pathToRoute(path) {
|
|
68
|
+
if (this.options.views[path]) return path;
|
|
69
|
+
|
|
70
|
+
const sorted = Object.keys(this.options.views).sort((a, b) => {
|
|
71
|
+
const aParams = (a.match(/:/g) || []).length;
|
|
72
|
+
const bParams = (b.match(/:/g) || []).length;
|
|
73
|
+
return aParams !== bParams ? aParams - bParams : b.length - a.length;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return sorted.find(route => routeToRegex(route).test(path)) || path;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extracts route parameters from a path.
|
|
81
|
+
* @param {string} [path] - Path to parse, defaults to current path
|
|
82
|
+
* @returns {object} Object containing route parameters
|
|
83
|
+
*/
|
|
84
|
+
parseRouteParameters(path = this.path) {
|
|
85
|
+
const route = this.pathToRoute(path);
|
|
86
|
+
const routeRegex = routeToRegex(route);
|
|
87
|
+
let routeParameterValues = path.match(routeRegex);
|
|
88
|
+
let routeParameterKeys = route.match(routeRegex);
|
|
89
|
+
const parameters = {};
|
|
90
|
+
|
|
91
|
+
if (!routeParameterValues || !routeParameterKeys) return parameters;
|
|
92
|
+
|
|
93
|
+
routeParameterValues = routeParameterValues.slice(1);
|
|
94
|
+
routeParameterKeys = routeParameterKeys.slice(1);
|
|
95
|
+
|
|
96
|
+
routeParameterValues.forEach((parameter, index) => {
|
|
97
|
+
const name = routeParameterKeys[index].slice(1);
|
|
98
|
+
|
|
99
|
+
parameters[name] = parameter;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return parameters;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
build() {
|
|
106
|
+
const reRenderView = () => this.renderView();
|
|
107
|
+
|
|
108
|
+
window.addEventListener('popstate', reRenderView);
|
|
109
|
+
if (this.options.mode !== 'history') window.addEventListener('hashchange', reRenderView);
|
|
110
|
+
this.replaceCleanup('popstate', () => {
|
|
111
|
+
window.removeEventListener('popstate', reRenderView);
|
|
112
|
+
if (this.options.mode !== 'history') window.removeEventListener('hashchange', reRenderView);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.renderView();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
renderView(route = this.route || this.options.defaultPath) {
|
|
119
|
+
if (this.currentRoute === route) return;
|
|
120
|
+
|
|
121
|
+
this.options.onRenderView?.(route);
|
|
122
|
+
|
|
123
|
+
this.view?.destroy?.();
|
|
124
|
+
this.empty();
|
|
125
|
+
|
|
126
|
+
if (!this.path) return (this.path = route);
|
|
127
|
+
|
|
128
|
+
this.currentRoute = route;
|
|
129
|
+
|
|
130
|
+
if (this.options.views[route]) {
|
|
131
|
+
this.view = new this.options.views[route]({ appendTo: this.elem, ...this.parseRouteParameters() });
|
|
132
|
+
} else if (this.options.notFound) this.view = new this.options.notFound({ appendTo: this.elem, route });
|
|
133
|
+
else if (this.options.defaultPath && this.path !== this.options.defaultPath) this.path = this.options.defaultPath;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default Router;
|
|
138
|
+
|
|
139
|
+
// Zero-arg scenarios for LLD verification
|
|
140
|
+
export const hashStripsQueryString = () => {
|
|
141
|
+
const r = new Router({ views: { '/path': Component }, autoRender: false });
|
|
142
|
+
window.location.hash = '#/path?query=1&sort=name';
|
|
143
|
+
return r.path;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const paramExtractedFromRoute = () => {
|
|
147
|
+
const r = new Router({ views: { '/users/:id': Component }, autoRender: false });
|
|
148
|
+
window.location.hash = '#/users/42';
|
|
149
|
+
return r.parseRouteParameters('/users/42')?.id;
|
|
150
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Router
|
|
2
|
+
|
|
3
|
+
> ./Router.js
|
|
4
|
+
|
|
5
|
+
Hash-based router that maps URL fragments to component classes. The design decision: navigating to the current route is a no-op; the active view is not destroyed and rebuilt when the URL doesn't change.
|
|
6
|
+
|
|
7
|
+
## Query strings are stripped before route matching
|
|
8
|
+
|
|
9
|
+
**method:** `hashStripsQueryString`
|
|
10
|
+
|
|
11
|
+
- `#/path?foo=bar` matches the `/path` route; query strings are stripped before matching so callers write route patterns without accounting for query parameters
|
|
12
|
+
- hashStripsQueryString() → "/path"
|
|
13
|
+
|
|
14
|
+
## Route parameters are extracted and passed to the rendered view
|
|
15
|
+
|
|
16
|
+
**method:** `paramExtractedFromRoute`
|
|
17
|
+
|
|
18
|
+
- patterns like `/users/:id` match `/users/42` and produce `{ id: '42' }` for the view without any URL parsing at the view level
|
|
19
|
+
- paramExtractedFromRoute() → "42"
|
|
20
|
+
|
|
21
|
+
## Same-route navigation does not rebuild the view
|
|
22
|
+
|
|
23
|
+
- if the new hash resolves to the same route as what's currently rendered, the component skips re-render
|
|
24
|
+
- this preserves view state across same-route navigations without the caller guarding against them
|
|
25
|
+
- does navigating to the currently active route leave the rendered view unchanged?
|
|
26
|
+
|
|
27
|
+
## Unmatched routes fall back rather than rendering nothing
|
|
28
|
+
|
|
29
|
+
- an unmatched hash tries `defaultPath` before showing the notFound component
|
|
30
|
+
- the notFound component is only shown when no fallback matches either
|
|
31
|
+
- does navigating to an unknown route eventually show the notFound component?
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Component } from '../../Component';
|
|
2
|
+
import { styled } from '../../styled';
|
|
3
|
+
|
|
4
|
+
const View = styled(
|
|
5
|
+
Component,
|
|
6
|
+
() => `
|
|
7
|
+
height: 100%;
|
|
8
|
+
width: 100%;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
flex: 1;
|
|
12
|
+
`,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export default View;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const routeToPath = (route, parameters) => {
|
|
2
|
+
let path = route;
|
|
3
|
+
|
|
4
|
+
if (parameters) {
|
|
5
|
+
Object.keys(parameters).forEach(key => {
|
|
6
|
+
path = path.replace(new RegExp(`:${key}`), parameters[key]);
|
|
7
|
+
});
|
|
8
|
+
} else {
|
|
9
|
+
path = path.replaceAll(/\/?:[^/]+/g, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return path;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const routeToRegex = route => {
|
|
16
|
+
return new RegExp(`^${route.replaceAll(/:[^/]+/g, '([^/]+)')}$`);
|
|
17
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Select
|
|
2
|
+
|
|
3
|
+
Select dropdown component extending Input, with dynamic option rendering and enhanced value access for both string and object-based option lists.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Select } from '@vanilla-bean/components';
|
|
9
|
+
|
|
10
|
+
const select = new Select({
|
|
11
|
+
options: ['one', 'two', 'three'],
|
|
12
|
+
value: 'two',
|
|
13
|
+
onChange: ({ value }) => console.log('selected:', value),
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Options
|
|
18
|
+
|
|
19
|
+
Select inherits all options from `Input`. Key additions:
|
|
20
|
+
|
|
21
|
+
| Option | Type | Default | Description |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `options` | `Array<string\|object>` | — | List of selectable options. See shape below. Reactive: reassigning `options.options` rebuilds the `<option>` elements |
|
|
24
|
+
| `value` | `any` | — | Currently selected value. Matched against option `value` attributes |
|
|
25
|
+
| `onChange` | `Function` | — | Called with an enhanced event containing `.value` (the selected value string) on change |
|
|
26
|
+
|
|
27
|
+
All other `Input` options (`placeholder`, `validations`, `onInput`, etc.) are available but less commonly used with Select.
|
|
28
|
+
|
|
29
|
+
### `options` entry shape
|
|
30
|
+
|
|
31
|
+
Each entry in the `options` array is either:
|
|
32
|
+
|
|
33
|
+
- A **string**: used as both the `label` and `value` of the `<option>` element
|
|
34
|
+
- An **object**: spread directly onto the `<option>` element. Common fields:
|
|
35
|
+
|
|
36
|
+
| Field | Type | Description |
|
|
37
|
+
| ---------- | --------- | ----------------------------------------- |
|
|
38
|
+
| `value` | `any` | The value submitted/returned on selection |
|
|
39
|
+
| `label` | `string` | Display text shown in the dropdown |
|
|
40
|
+
| `disabled` | `boolean` | Disables this specific option |
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
options: ['plain string', { label: 'Display Text', value: 42 }, { label: 'Unavailable', value: 'x', disabled: true }];
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Methods
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
select.value // getter — returns value, label, or textContent of the selected <option>
|
|
50
|
+
select.value = newVal // setter — sets elem.value directly
|
|
51
|
+
|
|
52
|
+
// Inherited from Input:
|
|
53
|
+
select.validate({ validations?, value? }): Array | undefined
|
|
54
|
+
select.isDirty: boolean
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Relationship to Input
|
|
58
|
+
|
|
59
|
+
`Select` extends `Input` and overrides `_setOption` only for the `options` key (to rebuild `<option>` children). All other option processing delegates to `Input`, which delegates to `Component`. The `tag` defaults to `'select'` rather than `'input'`. Type auto-detection from value type does not apply.
|
|
60
|
+
|
|
61
|
+
## Events
|
|
62
|
+
|
|
63
|
+
Select emits standard enhanced DOM events via inherited `Input` behavior. The `onChange` handler receives an event with `.value` set to the currently selected option's value.
|
|
64
|
+
|
|
65
|
+
## Example
|
|
66
|
+
|
|
67
|
+
Mixed string and object options with a numeric value, used inside a Form:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { Select } from '@vanilla-bean/components';
|
|
71
|
+
|
|
72
|
+
const select = new Select({
|
|
73
|
+
options: [
|
|
74
|
+
{ label: 'Low', value: 1 },
|
|
75
|
+
{ label: 'Medium', value: 2 },
|
|
76
|
+
{ label: 'High', value: 3 },
|
|
77
|
+
],
|
|
78
|
+
value: 2,
|
|
79
|
+
onChange: ({ value }) => {
|
|
80
|
+
select.options.value = value;
|
|
81
|
+
console.log('Priority level:', select.value); // uses getter
|
|
82
|
+
},
|
|
83
|
+
appendTo: document.body,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Swap the option list reactively at any time
|
|
87
|
+
select.options.options = ['alpha', 'beta', 'gamma'];
|
|
88
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Elem } from '../../Elem';
|
|
2
|
+
import { Input } from '../Input';
|
|
3
|
+
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
tag: 'select',
|
|
6
|
+
get priorityOptions() {
|
|
7
|
+
return new Set(['textContent', 'content', 'appendTo', 'prependTo', 'options']);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Select dropdown component extending Input with dynamic option management.
|
|
13
|
+
*
|
|
14
|
+
* Provides enhanced HTML select functionality with dynamic option rendering
|
|
15
|
+
* and improved value handling for both string and object-based options.
|
|
16
|
+
* @param {object} [options={}] - Select configuration options
|
|
17
|
+
* @param {string} [options.tag='select'] - HTML tag, uses select element
|
|
18
|
+
* @param {Array<string|object>} [options.options] - Array of select options
|
|
19
|
+
* @param {*} [options.value] - Currently selected value
|
|
20
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
21
|
+
* @returns {Select} Select component instance
|
|
22
|
+
*/
|
|
23
|
+
class Select extends Input {
|
|
24
|
+
defaultOptions = { ...super.defaultOptions, ...defaultOptions };
|
|
25
|
+
|
|
26
|
+
constructor(options = {}, ...children) {
|
|
27
|
+
super({ ...defaultOptions, ...options }, ...children);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static handlers = {
|
|
31
|
+
options(value) {
|
|
32
|
+
this.empty();
|
|
33
|
+
|
|
34
|
+
if (!value) return;
|
|
35
|
+
|
|
36
|
+
for (const option of value) {
|
|
37
|
+
// Optgroup: { label: 'Group', options: [...] }
|
|
38
|
+
if (typeof option === 'object' && Array.isArray(option.options)) {
|
|
39
|
+
const group = document.createElement('optgroup');
|
|
40
|
+
if (option.label) group.label = option.label;
|
|
41
|
+
for (const o of option.options) {
|
|
42
|
+
group.append(new Elem({ tag: 'option', ...(typeof o === 'object' ? o : { label: o, value: o }) }).elem);
|
|
43
|
+
}
|
|
44
|
+
this.elem.append(group);
|
|
45
|
+
} else {
|
|
46
|
+
this.append(
|
|
47
|
+
new Elem({ tag: 'option', ...(typeof option === 'object' ? option : { label: option, value: option }) }),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gets the currently selected value with enhanced option handling.
|
|
56
|
+
* @returns {*} Selected option value, label, or text content
|
|
57
|
+
*/
|
|
58
|
+
get value() {
|
|
59
|
+
// Use elem.options (HTMLOptionsCollection) — works across optgroups
|
|
60
|
+
const selected = Array.from(this.elem.options).find(({ selected }) => selected);
|
|
61
|
+
|
|
62
|
+
return selected?.value ?? selected?.label ?? selected?.textContent ?? this.elem.value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sets the selected value.
|
|
67
|
+
* @param {*} newValue - Value to select
|
|
68
|
+
*/
|
|
69
|
+
set value(newValue) {
|
|
70
|
+
this.elem.value = newValue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default Select;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Select
|
|
2
|
+
|
|
3
|
+
> ./Select.js
|
|
4
|
+
|
|
5
|
+
Dropdown select that extends Input. The design decision: Select inherits Input's value lifecycle; `isDirty`, validations, and onChange work the same way for a Select as they do for a text Input. Callers don't learn a separate API.
|
|
6
|
+
|
|
7
|
+
## Select inherits the full Input value contract
|
|
8
|
+
|
|
9
|
+
- `isDirty`, validations, and onChange all work the same way as Input without any Select-specific API
|
|
10
|
+
- does a Select have isDirty?
|
|
11
|
+
- does a Select value change trigger onChange the same way as an Input?
|
|
12
|
+
|
|
13
|
+
## Options map in order, preserving sequence
|
|
14
|
+
|
|
15
|
+
- items in the `options` array become select options in the same order; no automatic sorting or reindexing
|
|
16
|
+
- does the options array order match the dropdown order?
|
|
17
|
+
|
|
18
|
+
## The value getter returns the selected option's value, with label as fallback
|
|
19
|
+
|
|
20
|
+
- `select.value` returns the option's `value` attribute; if absent it falls back to `label`, then `textContent`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Select } from './Select';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Table
|
|
2
|
+
|
|
3
|
+
Data table component with configurable columns, client-side sorting, optional footer rows, and custom cell rendering.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Table } from '@vanilla-bean/components';
|
|
9
|
+
|
|
10
|
+
const table = new Table({
|
|
11
|
+
data: [
|
|
12
|
+
{ name: 'Alice', score: 42 },
|
|
13
|
+
{ name: 'Bob', score: 17 },
|
|
14
|
+
],
|
|
15
|
+
columns: ['name', 'score'],
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Options
|
|
20
|
+
|
|
21
|
+
| Option | Type | Default | Description |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `data` | `Array<object>` | — | Array of row data objects. Reactive: reassigning `options.data` rebuilds the tbody |
|
|
24
|
+
| `columns` | `Array<string\|object>` | `[]` | Column definitions; see shape below |
|
|
25
|
+
| `footer` | `Array<string\|object>` | — | Footer row cells, same shape as columns. Strings are capitalized automatically |
|
|
26
|
+
| `sortProperty` | `string` | — | Key of the currently sorted column |
|
|
27
|
+
| `sortDirection` | `string` | — | `'asc'` or `'desc'` |
|
|
28
|
+
| `onSort` | `Function` | built-in | Called with `(property, direction)` when a sortable column header is clicked. Default sorts `options.data` in place using `orderBy` |
|
|
29
|
+
|
|
30
|
+
### `columns` entry shape
|
|
31
|
+
|
|
32
|
+
A column can be a plain string (key name, auto-capitalized as header label) or an object:
|
|
33
|
+
|
|
34
|
+
| Field | Type | Description |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `key` | `string` | Property name on each row data object |
|
|
37
|
+
| `content` | `string\|Component` | Header cell content. Defaults to `capitalize(key)` |
|
|
38
|
+
| `sort` | `boolean` | Enables click-to-sort on this column's header with an animated sort icon |
|
|
39
|
+
| `dataColumn` | `object\|Function` | Options forwarded to each `<td>` component, or a `Function({ column, rowData, table })` returning those options |
|
|
40
|
+
| `...thOptions` | `any` | Any additional options forwarded to the `<th>` component (e.g., `style`, `className`) |
|
|
41
|
+
|
|
42
|
+
### `footer` entry shape
|
|
43
|
+
|
|
44
|
+
Same as columns: a string (rendered as capitalized text content) or an object with any `<td>` component options (e.g., `content`, `colspan`).
|
|
45
|
+
|
|
46
|
+
## Methods
|
|
47
|
+
|
|
48
|
+
Table does not expose imperative public methods. All mutations go through reactive options:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// Replace data (rebuilds tbody)
|
|
52
|
+
table.options.data = newDataArray;
|
|
53
|
+
|
|
54
|
+
// Trigger a sort programmatically
|
|
55
|
+
table.options.sortProperty = 'score';
|
|
56
|
+
table.options.sortDirection = 'asc';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Internal references after build:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
table.thead; // Elem wrapping <thead>
|
|
63
|
+
table.tbody; // Elem wrapping <tbody>
|
|
64
|
+
table.tfoot; // Elem wrapping <tfoot>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Events
|
|
68
|
+
|
|
69
|
+
Table does not emit custom events. Supply `onSort` to intercept sort interactions.
|
|
70
|
+
|
|
71
|
+
## Example
|
|
72
|
+
|
|
73
|
+
Sortable table with a custom column label, styled header, and footer totals:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { Table } from '@vanilla-bean/components';
|
|
77
|
+
import theme from '@vanilla-bean/components/theme';
|
|
78
|
+
|
|
79
|
+
const table = new Table({
|
|
80
|
+
data: [
|
|
81
|
+
{ item: 'Widget A', qty: 5, price: 9.99 },
|
|
82
|
+
{ item: 'Widget B', qty: 12, price: 4.49 },
|
|
83
|
+
],
|
|
84
|
+
columns: [
|
|
85
|
+
{ key: 'item', content: 'Product', sort: true },
|
|
86
|
+
{ key: 'qty', content: 'Quantity', sort: true, style: { textAlign: 'right' } },
|
|
87
|
+
{ key: 'price', content: 'Unit Price', style: { color: theme.colors.green } },
|
|
88
|
+
],
|
|
89
|
+
footer: [{ content: 'Totals:', colspan: 2 }, { content: '$59.83' }],
|
|
90
|
+
appendTo: document.body,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Clicking a sortable column header toggles direction between `'desc'` and `'asc'`. The `onSort` default mutates `options.data` in place, triggering a reactive tbody rebuild.
|