redgin 0.1.19 → 0.2.2

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/README.md CHANGED
@@ -1,268 +1,219 @@
1
- # Redgin
1
+ [![NPM](https://nodei.co/npm/redgin.svg?style=flat&data=v,d&color=red)](https://nodei.co/npm/redgin/)
2
2
 
3
- A lightweight (~5.3kb) library for building Web Components, compatible with Vanilla JS and all JavaScript frameworks. This library simplifies the creation of Web Components and offers features such as using JavaScript template literals for template syntax, rerendering elements with watch, creating getter/setters with getset, property reflection with propReflect, inline events with event, custom events with emit, injecting global styles with injectStyles, and support for TypeScript.
3
+ # RedGin
4
4
 
5
+ A lightweight (~5.3kb) library that solves the pain points of native Web Components. RedGin offers fine-grained reactivity, surgical updates, and intuitive APIs - making Web Components actually enjoyable to build.
5
6
 
7
+ ## Why RedGin?
6
8
 
7
- ## Features
9
+ Native Web Components are powerful but come with friction:
8
10
 
11
+ | Pain Point | Native Web Components | RedGin |
12
+ |------------|----------------------|--------|
13
+ | **Boilerplate** | Manual lifecycle callbacks, attributeChangedCallback, getters/setters | Zero boilerplate with `getset` and `propReflect` |
14
+ | **Reactivity** | Manual observation with `attributeChangedCallback` | Automatic reactivity with `watch`, `s()`, and fine-grained updates |
15
+ | **Template Updates** | Manual DOM manipulation | Surgical updates - only changed parts re-render |
16
+ | **Attribute Reflection** | Manual sync between properties and attributes | Automatic with `propReflect` |
17
+ | **Event Binding** | `addEventListener` boilerplate | Inline events with `on()` |
18
+ | **Style Sharing** | Duplicated styles per component | Global `shareStyle` injection |
19
+ | **Performance** | Full re-renders on any change | Only changed elements update |
20
+ | **TypeScript** | Complex typing for custom elements | First-class TypeScript support |
9
21
 
22
+ ## Core Philosophy
10
23
 
11
- - **JavaScript Template Literals for Template Syntax**: Simplify the creation of templates using JavaScript template literals. [Template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)
24
+ RedGin is built around **surgical updates** - only the elements that need to change, change. No virtual DOM, no heavy diffing, just precise, targeted updates to your components.
12
25
 
26
+ ## Key Features
13
27
 
14
-
15
- - **Rerender Element with Watch**: Easily trigger a rerender of an element by watching for changes.[`watch`](https://stackblitz.com/edit/typescript-t3fqo8?file=sampleWatch.ts)
16
-
17
-
18
-
19
- - **Create Getter/Setters with getset**: Define getter and setter functions for your properties.[`getset`](https://stackblitz.com/edit/typescript-t3fqo8?file=sampleWatch.ts)
20
-
21
-
22
-
23
- - **Create Property Reflection with propReflect**: Reflect property changes to corresponding attributes.[`propReflect`](https://stackblitz.com/edit/typescript-hlms7u?file=index.html)
24
-
25
-
26
-
27
- - **Create Inline Events with event**: Attach events directly in your component's template.[`event`](https://stackblitz.com/edit/typescript-t3fqo8?file=sampleWatch.ts)
28
-
29
-
30
-
31
- - **Create Custom Events with emit**: Emit custom events from your components.[`emit`](https://stackblitz.com/edit/redgin-childtoparent?file=index.ts)
32
-
33
-
34
-
35
- - **Inject Global Styles with injectStyles**: Apply global styles for your components.[`injectStyles`](https://stackblitz.com/edit/redgin-bootstrap?file=index.ts)
36
-
37
-
38
-
39
- - **Support for TypeScript**: Enjoy type safety when using Redgin with TypeScript.[Support Typescript](https://stackblitz.com/edit/typescript-ue61k6?file=index.ts)
40
-
41
-
42
-
43
-
44
- - **Build SPAs (Single-Page Applications)**: Simplify the development of SPAs using Redgin.[Single Page Application](https://stackblitz.com/edit/typescript-ezsw6j)
28
+ - **🎯 Surgical Rendering**: Update only what changes - perfect for large lists
29
+ - **📝 Template Literals**: Write components using familiar JS template syntax
30
+ - **⚡️ Fine-grained Reactivity**: Multiple reactivity patterns (`watch`, `s()`, `getset`, `propReflect`)
31
+ - **🔗 Attribute Binding**: Smart `attr()` helper for dynamic attributes
32
+ - **🔄 Property Reflection**: Sync properties with attributes using `propReflect`
33
+ - **📊 Reactive Getters/Setters**: Create reactive state with `getset`
34
+ - **🎨 Style Management**: Global style injection with `shareStyle` and scoped styles with `css`
35
+ - **📘 TypeScript Ready**: Full type safety and IntelliSense
45
36
 
46
37
  ## Installation
47
38
 
48
-
49
-
50
- Include the Redgin library in your project.
51
-
52
-
53
- ```html
54
- // via html
55
- <script type="module" src="https://cdn.jsdelivr.net/npm/redgin@latest/dist/redgin.min.js"></script>
56
-
57
- ```
58
-
59
-
60
- Or install it via npm:
61
-
62
-
63
-
39
+ ### Via npm
64
40
  ```bash
65
-
66
41
  npm i redgin
67
-
68
42
  ```
69
- ## Usage
70
-
71
-
72
-
73
- 1. **Import the Library:**
74
-
75
43
 
44
+ ## Via CDN
76
45
 
77
- ```javascript
78
-
79
- // via js
80
-
81
- import { Redgin, watch, getset, html } from 'https://cdn.jsdelivr.net/npm/redgin@latest/dist/redgin.min.js';
82
-
83
-
84
-
85
- // via npm
86
-
87
- import { Redgin, watch, getset, html } from 'redgin';
88
-
46
+ ```js
47
+ <script type="module" src="https://cdn.jsdelivr.net/npm/redgin@latest/dist/redgin.min.js"></script>
89
48
  ```
90
49
 
91
50
 
51
+ ## Quick Start
92
52
 
93
- 2. **Use the Features:**
94
-
95
-
96
-
97
- ```javascript
98
-
99
- // FetchApiComponent.ts
53
+ ```js
54
+ import { RedGin, getset, on, html } from 'redgin';
100
55
 
101
- // Creating a Fetch Api Component that displays Todos using Getset, Watch
102
- class FetchApi extends RedGin {
103
- // Reactive properties using getset
104
- ready = getset<boolean>(false);
105
- todos: any;
56
+ class Counter extends RedGin {
57
+ count = getset(0);
106
58
 
107
- // Initialize data from the API in the onInit lifecycle method
108
- onInit() {
109
- fetch('https://jsonplaceholder.typicode.com/todos/1')
110
- .then(response => response.json())
111
- .then(json => {
112
- this.todos = json;
113
- this.ready = true;
114
- });
115
- }
116
-
117
- // Render method for displaying the fetched data
118
- render() {
59
+ render() {
119
60
  return html`
120
- ${watch(['ready'],
121
- () => this.ready ? JSON.stringify(this.todos) : html`Loading...`
122
- )}`;
61
+ <button ${on('click', () => this.count++)}>
62
+ Count: ${this.count}
63
+ </button>
64
+ `;
123
65
  }
124
66
  }
125
67
 
126
- // Define the custom element 'fetch-api'
127
- customElements.define('fetch-api', FetchApi);
128
-
68
+ customElements.define('my-counter', Counter);
129
69
  ```
130
70
 
71
+ ## API Reference
131
72
 
132
- 3. **Passing data from Parent to Child component**
133
73
 
74
+ ### Core Helpers
134
75
 
76
+ | Helper | Purpose | Example |
77
+ | :--- | :--- | :--- |
78
+ | `getset(initial)` | Creates reactive property with getter/setter | `count = getset(0)` |
79
+ | `propReflect(initial)` | Reactive property that reflects to attribute | `theme = propReflect('light')` |
80
+ | `watch(deps, callback)` | Fine-grained control - rerenders when specified dependencies change | `${watch(['count', 'theme'], () => html`<div>...</div>`)}` |
81
+ | `s(callback)` | Shorthand for reactive value binding | `${s(() => this.count)}` |
82
+ | `attr(name, callback)` | Surgical attribute binding | `${attr('disabled', () => !this.editable)}` |
83
+ | `on(event, handler)` | Event listener binding | `${on('click', () => this.save())}` |
84
+ | `html` | Template literal tag for HTML | `html`<div>Hello</div>`` |
135
85
 
136
- ```javascript
86
+ ### Style Helpers
137
87
 
138
- // ParentToChildComponents.ts
88
+ | Helper | Purpose | Example |
89
+ | :--- | :--- | :--- |
90
+ | `css` | Template literal tag for component-scoped styles | `styles = [css`.card { padding: 1rem; }`]` |
91
+ | `shareStyle(styles)` | Injects global styles across all components | `shareStyle(css:host { --brand: blue; })` |
139
92
 
140
- class ParentComp extends RedGin {
141
- currentItem: string = 'Laptop';
142
93
 
143
- // Initialize child component with data using properties or attributes
144
- onInit() {
145
- // Option 1: Send data to child component using properties
146
- const child: IChild = this.shadowRoot?.querySelector('child-comp')!;
147
- child.item = this.currentItem;
148
- }
94
+ ## Lifecycle Methods
149
95
 
150
- // Render method for the parent component
151
- render() {
152
- return html`
153
- <child-comp></child-comp>
96
+ * onInit() - After first render
97
+ * onDoUpdate() - After data sync
98
+ * onUpdated() - After every attribute change/requestUpdate
154
99
 
155
- <!-- Option 2: Send data to child component using attributes -->
156
- <child2-comp item="${this.currentItem}"></child2-comp>
157
- `;
158
- }
159
- }
160
-
161
-
162
- ```
100
+ ## Style Management Examples
163
101
 
164
- 3. **Passing data from Child to Parent component**
165
- ``` javascript
102
+ ### Global Design System with shareStyle
103
+ import { RedGin, shareStyle, css, html } from 'redgin';
166
104
 
167
- // ParentChildComponents.ts
105
+ // Share Bootstrap globally (injected once, used everywhere)
106
+ shareStyle('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">')
168
107
 
169
- // Child component for emitting a custom event
170
- class ChildComp extends RedGin {
171
- render() {
172
- return html`
173
- <button ${event('click', () => emit.call(this, 'newItem', 'added New Item?'))}>
174
- <slot>Add to parent's list</slot>
175
- </button>
176
- `;
108
+ // Share design tokens across all components
109
+ ```js
110
+ shareStyle(css`
111
+ :host {
112
+ --brand-primary: #007bff;
113
+ --brand-success: #28a745;
114
+ --card-shadow: 0 4px 6px rgba(0,0,0,0.1);
177
115
  }
178
- }
116
+
117
+ .rg-card {
118
+ border-radius: 8px;
119
+ box-shadow: var(--card-shadow);
120
+ transition: transform 0.2s;
121
+ }
122
+ `);
123
+
124
+ class ProductCard extends RedGin {
125
+ // Component-specific styles (merged with shared styles)
126
+ styles = [css`
127
+ .local-price { color: var(--brand-success); font-weight: bold; }
128
+ `]
179
129
 
180
- // Parent component for receiving the custom event
181
- class ParentComp extends RedGin {
182
130
  render() {
131
+ // Bootstrap classes work without local import!
183
132
  return html`
184
- <child-comp
185
- ${event('newItem', (e: CustomEvent) => console.log(`Received child data: ${e.detail}`))}>
186
- Get Child Data?
187
- </child-comp>
133
+ <div class="rg-card p-3">
134
+ <div class="local-price">$120.00</div>
135
+ <button class="btn btn-primary">Buy Now</button>
136
+ </div>
188
137
  `;
189
138
  }
190
139
  }
191
-
192
140
  ```
193
141
 
194
- 4. Creating a Reactive button
142
+ ## Reactivity Patterns: watch vs s()
195
143
 
196
- ```javascript
144
+ ### RedGin offers two complementary reactivity patterns:
197
145
 
198
- // ReactiveButton.ts
199
-
200
- class ReactiveButton extends RedGin {
201
- // Reactive property using propReflect
202
- message = propReflect<string>('Hello, World!');
203
-
204
- // Observed attributes for the component
205
- static observedAttributes = ['message'];
146
+ ## s() - Smart Auto-detection
147
+ ```js
148
+ // Automatically tracks dependencies
149
+ render() {
150
+ return html`
151
+ <div>${s(() => this.count)}</div>
152
+ <div>${s(() => this.theme)}</div>
153
+ `;
154
+ }
155
+ ```
206
156
 
207
- // Render method for the component
208
- render() {
209
- // Use watch to trigger a rerender when 'message' changes
210
- return html`${watch(['message'], () => html`
211
- <button type="button">${this.message}</button>
157
+ ## watch - Explicit Control
158
+ ```js
159
+ // Fine-grained control over dependencies
160
+ render() {
161
+ return html`
162
+ ${watch(['count', 'theme'], () => html`
163
+ <div class="${this.theme}">
164
+ Count: ${this.count}
165
+ </div>
212
166
  `)}
213
- `;
214
- }
215
- }
216
-
167
+ `;
168
+ }
217
169
  ```
218
170
 
219
- 5. For Loop through the list of Products
220
- ``` javascript
221
-
222
- // ProductListRenderer.ts
223
-
224
- // For Loop through the List of Products
225
- class ProductListRenderer extends RedGin {
226
- // Reactive property using getset
227
- products = getset<IProduct[]>([
228
- { id: 1, name: 'Laptop' },
229
- { id: 2, name: 'Camera' },
230
- ]);
231
-
232
- // Render method for displaying the list of products
233
- render() {
234
- return html`
235
- <ul>
236
- ${watch(['products'], () =>
237
- this.products.map(product =>
238
- html`<li>${product.id} - ${product.name}</li>`
239
- )
240
- )}
241
- </ul>`;
242
- }
243
- }
244
171
 
172
+ ## Examples
245
173
 
174
+ Check out these live examples demonstrating RedGin's capabilities:
246
175
 
247
- ```
248
- More
176
+ ### Basic Examples
177
+ * [Simple Counter](https://github.com/josnin/redgin/tree/Dev/samples) - Getting started with RedGin
178
+ * [Two-way Data Binding](https://github.com/josnin/redgin/tree/Dev/samples) - Using getset and events
179
+ * [Todo App](https://github.com/josnin/redgin/tree/Dev/samples) - Classic todo example
249
180
 
250
181
 
182
+ ### Style Examples
251
183
 
184
+ * [Bootstrap Integration](https://github.com/josnin/redgin/tree/Dev/samples) - Using shareStyle with CSS frameworks
185
+ * [Design Tokens](https://github.com/josnin/redgin/tree/Dev/samples) - Global theme variables
186
+ * [Scoped Styles](https://github.com/josnin/redgin/tree/Dev/samples) - Component-specific CSS
252
187
 
253
188
 
189
+ ### Advanced Patterns
190
+ * [Surgical List Updates (1,000+ items)](https://github.com/josnin/redgin/tree/Dev/samples) - Only updated rows re-render
191
+ * [E-commerce Application](https://github.com/josnin/redgin/tree/Dev/samples) - Cart, checkout, and async operations
192
+ * [CRM Dashboard](https://github.com/josnin/redgin/tree/Dev/samples) - Multi-view with modals and pipeline
193
+ * [Parent-Child Communication](https://github.com/josnin/redgin/tree/Dev/samples) - Custom events and props
254
194
 
255
- ## For VSCode Syntax Highlight template literals
195
+ ### Integration Examples
256
196
 
257
- ### Install extension [inline-html](https://marketplace.visualstudio.com/items?itemName=pushqrdx.inline-html)
197
+ * [TypeScript Support](https://github.com/josnin/redgin/tree/Dev/samples) - Full type safety
198
+ * [With Bootstrap](https://github.com/josnin/redgin/tree/Dev/samples) - Using CSS frameworks
199
+ * [Property Reflection](https://github.com/josnin/redgin/tree/Dev/samples) - Syncing props with attributes
258
200
 
259
- ```js
260
- render() {
261
- return html`<div>with syntax highlighted</div>`
262
- }
263
- ```
201
+ ## Performance
264
202
 
203
+ * Surgical Updates: Only changed elements re-render
204
+ * Bundle Size: ~5.3kb minified + gzipped
205
+ * Memory: Zero virtual DOM overhead
206
+ * Style Injection: Global styles shared once, not duplicated per component
265
207
 
208
+ ## Contributing
209
+
210
+ We welcome contributions!
211
+ ```
212
+ git clone https://github.com/josnin/redgin.git
213
+ cd redgin
214
+ npm install
215
+ npm run dev
216
+ ```
266
217
 
267
218
  ## Reference
268
219
  https://web.dev/custom-elements-best-practices/
@@ -270,14 +221,6 @@ https://web.dev/custom-elements-best-practices/
270
221
  https://web.dev/shadowdom-v1/
271
222
 
272
223
 
273
- ## How to run development server?
274
- ```
275
- git clone git@github.com:josnin/redgin.git
276
- cd ~/Documents/redgin/
277
- npm install
278
- npm run dev
279
- ```
280
-
281
224
  ## Help
282
225
 
283
226
  Need help? Open an issue in: [ISSUES](https://github.com/josnin/redgin/issues)
@@ -285,3 +228,5 @@ Need help? Open an issue in: [ISSUES](https://github.com/josnin/redgin/issues)
285
228
 
286
229
  ## Contributing
287
230
  Want to improve and add feature? Fork the repo, add your changes and send a pull request.
231
+
232
+
@@ -0,0 +1,11 @@
1
+ /**
2
+ * watchProp: Specifically for component attributes/props.
3
+ * Returns a marker string that the RedGin core parses as a "Directive on a Tag".
4
+ */
5
+ /**
6
+ * Type definition for the reactive expression.
7
+ * 'this' is typed as the component instance.
8
+ */
9
+ type WatchExpression<T = any> = (this: T) => any;
10
+ export declare const attr: (attrName: string, exp: WatchExpression) => string;
11
+ export {};
@@ -1,4 +1,16 @@
1
- export declare const event: (type: string, fn: any) => string;
1
+ export type EventHandler = (this: any, e: any) => any;
2
+ export declare const event: (type: string, fn: EventHandler) => string;
3
+ /**
4
+ * Alias for 'event'. Moving forward, use 'on' for a more
5
+ * declarative feel (e.g., on('click', ...))
6
+ */
7
+ export declare const on: (type: string, fn: EventHandler) => string;
2
8
  export declare function emit(this: any, customEvent: string, value: any, options?: CustomEvent): void;
9
+ /**
10
+ * Attach all events for this component
11
+ */
3
12
  export declare function applyEventListeners(this: any): void;
13
+ /**
14
+ * Remove all events for this component
15
+ */
4
16
  export declare function removeEventListeners(this: any): void;
@@ -1,3 +1,5 @@
1
1
  export * from './watch';
2
+ export * from './stream';
3
+ export * from './attr';
2
4
  export * from './events';
3
5
  export * from './directives';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Type definition for the reactive expression.
3
+ * 'this' is typed as the component instance.
4
+ */
5
+ type WatchExpression<T = any> = (this: T) => any;
6
+ /**
7
+ * The 's' (stream) utility: Provides "Auto-Tracked" reactivity.
8
+ *
9
+ * Unlike standard 'watch', it treats the expression as a dynamic stream.
10
+ * It surgically extracts dependencies via regex from the function source,
11
+ * removing the need for manual reference arrays.
12
+ *
13
+ * It creates a placeholder <in-watch> and registers it to the component
14
+ * instance for O(1) targeted updates.
15
+ */
16
+ export declare const s: (exp: WatchExpression) => string;
17
+ export {};
@@ -1,2 +1,10 @@
1
- export declare const watch: (ref: string[], exp: any) => string;
2
- export declare function watchFn(this: any, _prop: string): boolean;
1
+ /**
2
+ * Type definition for the reactive expression.
3
+ * 'this' is typed as the component instance.
4
+ */
5
+ export type WatchExpression<T = any> = (this: T) => any;
6
+ /**
7
+ * The 'watch' utility used inside render().
8
+ * It creates a placeholder and registers dependencies to the current component instance.
9
+ */
10
+ export declare const watch: (refs: string[], exp?: WatchExpression) => string;
@@ -1,5 +1,13 @@
1
+ /**
2
+ * Options for the getset behavior.
3
+ * forWatch: determines if changing this property should trigger a DOM update.
4
+ */
1
5
  interface IGetSet {
2
6
  forWatch?: boolean;
3
7
  }
8
+ /**
9
+ * Factory function to mark a property as reactive state.
10
+ * Usage: myData = getset(0)
11
+ */
4
12
  export declare function getset<T>(value: T, options?: IGetSet): T;
5
13
  export {};
@@ -1,8 +1,16 @@
1
+ /**
2
+ * Interface for propReflect options.
3
+ * Allows custom conversion logic and explicit type declarations.
4
+ */
1
5
  interface IPropReflect<T = any> {
2
6
  serializerFn?: (this: any, prop: string, type: any, _default: T) => T;
3
7
  deserializerFn?: (this: any, prop: string, type: any, _default: T, value: T) => void;
4
8
  type?: any;
5
9
  name?: string;
6
10
  }
11
+ /**
12
+ * Marks a property for reflection.
13
+ * Usage: myProp = propReflect(false)
14
+ */
7
15
  export declare function propReflect<T>(value: T, options?: IPropReflect<T>): T;
8
16
  export {};
package/dist/redgin.d.ts CHANGED
@@ -1,25 +1,88 @@
1
- export { event, emit, watch, customDirectives } from './directives/index';
1
+ import { WatchExpression } from './directives/index';
2
+ export { on, event, // to obsolete
3
+ emit, s, watch, attr, customDirectives, } from './directives/index';
2
4
  export { getset, propReflect, customPropsBehavior } from './props/index';
3
- export declare const attachShadow: ShadowRootInit;
4
- export declare let injectStyles: string[];
5
- export declare let defaultStyles: string[];
6
- export declare const html: (raw: TemplateStringsArray, ...values: any[]) => string;
7
- export declare const css: (raw: TemplateStringsArray, ...values: any[]) => string;
5
+ export declare const shared: string[];
6
+ export declare const defaultStyle = ":host{display:block}";
7
+ /**
8
+ * Handles style injection with support for adoptedStyleSheets (faster memory sharing)
9
+ * and standard <style>/<link> fallbacks.
10
+ */
11
+ export declare function _applyStyle(styles: string | string[], shadowRoot?: ShadowRoot): string;
12
+ /**
13
+ * Add global styles that will be applied to every RedGin component
14
+ */
15
+ export declare function shareStyle(style: string): void;
16
+ /**
17
+ * THE SURGICAL FLATTENER:
18
+ * 1. Recursively flattens arrays.
19
+ * 2. Joins with EMPTY STRING '' (kills the comma). --> Note: watcher do this
20
+ * 3. Filters out 'dead' values (null, undefined, false).
21
+ */
22
+ export declare const _f: (v: any) => string;
23
+ export declare const html: (raw: TemplateStringsArray, ...vals: any[]) => string;
24
+ export declare const safe: (val: any) => string;
25
+ export declare const css: (raw: TemplateStringsArray, ...vals: any[]) => string;
8
26
  export declare class RedGin extends HTMLElement {
27
+ private _pending;
28
+ private _changed;
29
+ private _connected;
30
+ private _reactiveCache;
31
+ /**
32
+ * INSTANCE-LEVEL CACHING
33
+ * These Maps allow O(1) lookups for data-binding.
34
+ * We store direct references to HTMLElements so we never use querySelector during updates.
35
+ */
36
+ _watchRegistry: Map<string, Map<string, WatchExpression<any>>>;
37
+ _idToProps: Map<string, string[]>;
38
+ _watchElements: Map<string, HTMLElement>;
39
+ _attrRegistry: Map<string, Map<string, WatchExpression<any>>>;
40
+ _attrElements: Map<string, HTMLElement>;
41
+ _eventElements: Map<string, HTMLElement>;
42
+ styles: string[];
9
43
  constructor();
10
44
  connectedCallback(): void;
11
- attributeChangedCallback(prop: any, oldValue: any, newValue: any): void;
12
45
  disconnectedCallback(): void;
13
- private updateContents;
14
- private setEventListeners;
15
- private setPropsBehavior;
16
- getStyles(styles: string[]): string;
17
- private _onInit;
18
- private _onDoUpdate;
19
- private _onUpdated;
46
+ attributeChangedCallback(prop: string, oldV: any, newV: any): void;
47
+ /**
48
+ * Schedules a DOM update using a Microtask.
49
+ * If 5 properties change at once, only 1 DOM update is triggered.
50
+ */
51
+ protected requestUpdate(prop: string): void;
52
+ /**
53
+ * The "Tick" where DOM updates actually happen.
54
+ */
55
+ private _flush;
56
+ /**
57
+ * Initial setup: Sets up props, applies styles, renders HTML, and caches DOM nodes.
58
+ */
59
+ private _init;
60
+ private _collectElements;
61
+ /**
62
+ * Garbage collection: Removes watcher references when an <in-watch> element is removed.
63
+ */
64
+ _cleanupWatch(uniqId: string): void;
65
+ /**
66
+ * First-time synchronization of property values to DOM.
67
+ */
68
+ private _sync;
69
+ /**
70
+ * Core update logic: Calls registered directives (like watchFn)
71
+ */
72
+ private _update;
73
+ /**
74
+ * Lifecycle hook triggered after DOM updates are finished.
75
+ */
76
+ private _afterUpdate;
77
+ private _afterUpdateNoDomChange;
78
+ /**
79
+ * Identifies all class properties to be made reactive.
80
+ * Caches the list to avoid repeat CPU-heavy property reflection.
81
+ */
82
+ private _setupProps;
83
+ private _reactiveProps;
20
84
  onInit(): void;
21
85
  onDoUpdate(): void;
22
86
  onUpdated(): void;
23
- styles: string[];
24
87
  render(): string;
25
88
  }
@@ -1,13 +1,2 @@
1
- var O=Object.defineProperty;var $=(s,t,e)=>t in s?O(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var f=(s,t,e)=>($(s,typeof t!="symbol"?t+"":t,e),e);var d=()=>crypto.randomUUID().split("-")[0],v=s=>s.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`),u=s=>s.replace(/-./g,t=>t[1].toUpperCase());function x(s){let t=[];for(let e of h.reg)t.push(e.call(this,s));return t.filter(e=>e===!0).length>0}var y=class{static define(t){y.reg.push(t)}},h=y;f(h,"reg",[]);var p={},m=class extends HTMLElement{};customElements.get("in-watch")||customElements.define("in-watch",m);var L=(s,t)=>{let e=document.createElement("in-watch"),o=d();for(let r of s)Object.hasOwn(p,r)||(p[r]={}),p[r][o]=t;return e.dataset.watch__=o,e.outerHTML};function _(s){let t=u(s),e=!1;if(Object.hasOwn(p,t)){for(let o of Object.keys(p[t]))if(this.shadowRoot){let r=this.shadowRoot.querySelector(`[data-watch__="${o}"]`);r&&(r.innerHTML=p[t][o]?p[t][o].call(this):this[t],e=!0)}}return e}h.define(_);var S=[];var C=(s,t)=>{let e=d();return S.push([s,t,e]),`data-evt__=${e}`};function I(s,t,e){let o={detail:t,composed:!0},r=new CustomEvent(s,{...o,...e});this.shadowRoot&&this.shadowRoot.dispatchEvent(r)}function E(s){for(let t of S){let[e,o,r]=t;if(this.shadowRoot){let i=this.shadowRoot.querySelector(`[data-evt__="${r}"]`);i&&(s===0?i.addEventListener(e,o):i.removeEventListener(e,o))}}}function R(){E.call(this,0)}function T(){E.call(this,1)}function U(s,t){for(let e of c.reg)e.call(this,s,t)}var b=class{static define(t){b.reg.push(t)}},c=b;f(c,"reg",[]);var D=["^class$","^style$","^className$","^classList$","^id$","^dataset$","^data-","^aria-"],g=["disabled"],P=s=>{let t=!0;for(let e of D){let o=new RegExp(e,"g");if(s.match(o)){t=!1,console.error(`Please remove attribute '${s}' in the observedAttributes,
2
- DOM already provided built-in props reflection for this attribute.`);break}}return t};function j(s,t){if(t===void 0||t?.name!="propReflect")return;let{type:e,value:o,serializerFn:r,deserializerFn:i}=t,l=this.constructor.observedAttributes,w=u(s),n=v(s);if(l===void 0||!l.includes(n)){console.error(`Unable to apply propReflect '${w}' for attribute '${n}',
3
- Please add '${n}' in the observedAttributes of ${this.constructor.name} component`);return}!P(n)||Object.defineProperty(this,w,{configurable:!0,set(a){if(i)return i.call(this,n,e,o,a);(e===Boolean||typeof a=="boolean"||g.includes(n))&&a===!0?this.setAttribute(n,""):(e===Boolean||g.includes(n))&&a===!1?this.removeAttribute(n):([Object,Array].includes(e)||["object","array"].includes(typeof a))&&a?this.setAttribute(n,JSON.stringify(a)):([String,Number].includes(e)||["string","number"].includes(typeof a))&&a?this.setAttribute(n,a):this.removeAttribute(n)},get(){if(r)return r.call(this,n,e,o);if(n in g||e===Boolean||typeof o=="boolean")return this.hasAttribute(n);if(([String,Array,Object].includes(e)||["number","string","array","object"].includes(typeof o))&&!this.hasAttribute(n))return o;if((e===String||typeof o=="string")&&this.hasAttribute(n))return this.getAttribute(n);if((e===Number||typeof o=="number")&&this.hasAttribute(n))return Number(this.getAttribute(n));if(([Array,Object].includes(e)||["array","object"].includes(typeof o))&&this.hasAttribute(n))return JSON.parse(this.getAttribute(n))}})}function k(s,t){return{value:s,...t,name:"propReflect"}}c.define(j);function B(s,t){if(t===void 0||t?.name!="getset")return;let{value:e,forWatch:o}=t;this[`#${s}`]=e,Object.defineProperty(this,s,{configurable:!0,set(r){this[`#${s}`]=r,o&&this.updateContents(s)&&this._onUpdated()},get(){return this[`#${s}`]}})}function M(s,t){return{value:s,...{forWatch:!0},...t,name:"getset"}}c.define(B);var N={mode:"open",delegatesFocus:!0},q=[],F=[` /* Custom elements are display: inline by default,
4
- * so setting their width or height will have no effect
5
- */
6
- :host { display: block; }
7
- `],H=(s,...t)=>String.raw({raw:s},...t),bt=H,A=class extends HTMLElement{constructor(){super(),this.attachShadow(N)}connectedCallback(){this._onInit(),this._onDoUpdate()}attributeChangedCallback(t,e,o){if(e===o)return;this.updateContents(t)&&this._onUpdated()}disconnectedCallback(){T.call(this)}updateContents(t){return x.call(this,t)}setEventListeners(){R.call(this)}setPropsBehavior(){let t=Object.getOwnPropertyNames(this).filter(e=>e!="styles");for(let e of t){let o=this[e];U.call(this,e,o)}}getStyles(t){let e=[],o=[],r=this.shadowRoot?.adoptedStyleSheets;for(let i of t)if(i.startsWith("<link"))e.push(i);else if(i.startsWith("@import")||!r){let l=document.createElement("style");l.innerHTML=i,e.push(l.outerHTML)}else{let l=new CSSStyleSheet;l.replaceSync(i),o.push(l)}return this.shadowRoot&&o.length>0&&(this.shadowRoot.adoptedStyleSheets=[...this.shadowRoot.adoptedStyleSheets,...o]),e.join("")}_onInit(){this.setPropsBehavior(),this.shadowRoot&&(this.shadowRoot.innerHTML=`
8
- ${this.getStyles(q)}
9
- ${this.getStyles(F)}
10
- ${this.getStyles(this.styles)}
11
- ${this.render()}
12
- `),this.onInit()}_onDoUpdate(){let t=Object.getOwnPropertyNames(this).filter(e=>e!="styles");for(let e of t)this.updateContents(e)&&this._onUpdated();this.setEventListeners(),this.onDoUpdate()}_onUpdated(){this.setEventListeners(),this.onUpdated()}onInit(){}onDoUpdate(){}onUpdated(){}styles=[];render(){return""}};export{A as RedGin,N as attachShadow,bt as css,h as customDirectives,c as customPropsBehavior,F as defaultStyles,I as emit,C as event,M as getset,H as html,q as injectStyles,k as propReflect,L as watch};
1
+ var U=Object.defineProperty;var W=(e,t,n)=>t in e?U(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var g=(e,t,n)=>(W(e,typeof t!="symbol"?t+"":t,n),n);function d(){return"id-"+Math.random().toString(16).slice(2)+"-"+Date.now()}var S=e=>e.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`),u=e=>e.replace(/-./g,t=>t[1].toUpperCase());function T(e){let t=[];for(let n of p.reg)t.push(n.call(this,e));return t.filter(n=>n===!0).length>0}var m=class{static define(t){m.reg.push(t)}},p=m;g(p,"reg",[]);var k=(e,t)=>{let n=d(),r=window.__redgin_current_instance;if(r){for(let s=0;s<e.length;s++){let a=e[s],c=r._watchRegistry.get(a);c||(c=new Map,r._watchRegistry.set(a,c)),c.set(n,t)}r._idToProps.set(n,e)}return`<in-watch data-watch="${n}"></in-watch>`},y=class extends HTMLElement{disconnectedCallback(){let t=this.dataset.watch,r=this.getRootNode()?.host;t&&r&&r._cleanupWatch(t)}};customElements.get("in-watch")||customElements.define("in-watch",y);var A=e=>Array.isArray(e)?e.map(A).join(""):e===void 0?"":String(e);p.define(function(t){let n=u(t),r=this._watchRegistry.get(n);if(!r)return!1;let s=!1;for(let[a,c]of r){let i=this._watchElements.get(a);if((!i||!i.isConnected)&&(i=this.shadowRoot.querySelector(`[data-watch="${a}"]`),i&&this._watchElements.set(a,i)),i){let o=c?c.call(this):this[n],l=A(o);i.innerHTML!==String(l)&&(i.innerHTML=l,s=!0)}}return s});var q=e=>{let t=d(),n=window.__redgin_current_instance;if(n){let r=e.toString(),s=/this\.([a-zA-Z_$][\w$]*)/g,a,c=new Set;for(;(a=s.exec(r))!==null;){let i=a[1];n._reactiveCache?.includes(i)&&c.add(i)}for(let i of c){let o=n._watchRegistry.get(i);o||(o=new Map,n._watchRegistry.set(i,o)),o.set(t,e)}n._idToProps.set(t,Array.from(c))}return`<in-watch data-watch="${t}"></in-watch>`};var I=(e,t)=>{let n=d(),r=window.__redgin_current_instance;if(r){let s=t.toString(),a=/this\.([a-zA-Z_$][\w$]*)/g,c,i=new Set;for(;(c=a.exec(s))!==null;){let o=c[1];r._reactiveCache&&r._reactiveCache.includes(o)&&i.add(o)}for(let o of i){r._attrRegistry||(r._attrRegistry=new Map);let l=r._attrRegistry.get(o);l||(l=new Map,r._attrRegistry.set(o,l)),l.set(n,{attrName:e,exp:t})}}return`rg-attr__${e}="${n}"`};p.define(function(t){let n=u(t),r=this._attrRegistry?.get(n);if(!r)return!1;let s=!1;for(let[a,c]of r){let i=this._attrElements.get(a);if((!i||!i.isConnected)&&(i=this.shadowRoot.querySelector(`[rg-attr__${c.attrName}="${a}"]`),i&&this._attrElements.set(a,i)),i){let o=c.exp?c.exp.call(this):this[n],l=c.attrName;if(o===!1||o===null||o===void 0)i.hasAttribute(l)&&(i.removeAttribute(l),s=!0);else{let _=o===!0?"":String(o);i.getAttribute(l)!==_&&(i.setAttribute(l,_),s=!0)}}}return s});var C=(e,t)=>{let n=d(),r=window.__redgin_current_instance;return r&&(r._eventRegistry||(r._eventRegistry=new Map),r._eventRegistry.set(n,[e,t])),`rg-evt__${e}="${n}"`},L=C;function O(e,t,n){let r={detail:t,composed:!0},s=new CustomEvent(e,{...r,...n});this.shadowRoot&&this.shadowRoot.dispatchEvent(s)}function w(){if(!(!this._eventElements||!this._eventRegistry))for(let[e,t]of this._eventElements){let n=this._eventRegistry.get(e);if(!n)continue;let[r,s]=n;t.addEventListener(r,s)}}function $(){if(!(!this._eventElements||!this._eventRegistry))for(let[e,t]of this._eventElements){let n=this._eventRegistry.get(e);if(!n)continue;let[r,s]=n;t.removeEventListener(r,s)}}function P(e,t){for(let n of f.reg)n.call(this,e,t)}var b=class{static define(t){b.reg.push(t)}},f=b;g(f,"reg",[]);var D=["^class$","^style$","^className$","^classList$","^dataset$","^data-","^aria-","^hidden$","^tabindex$","^slot$","^contenteditable$","^draggable$","^spellcheck$"],F=D.map(e=>new RegExp(e)),j=new Set(["disabled","hidden","checked","selected","readonly","multiple"]),H=e=>{for(let t of F)if(t.test(e))return console.error(`Please remove attribute '${e}' from observedAttributes; browser already provides built-in reflection.`),!1;return!0};function z(e,t){if(!t||t.name!=="propReflect")return;let{type:n,value:r,serializerFn:s,deserializerFn:a}=t,c=this.constructor.observedAttributes,i=u(e),o=S(e);if(!c?.includes(o)){console.error(`propReflect '${i}' cannot map to '${o}'. Add '${o}' to observedAttributes of ${this.constructor.name}.`);return}if(!H(o))return;let l=n===Boolean||j.has(o)||typeof r=="boolean",_=n===Object||n===Array||["object","array"].includes(typeof r),R=n===String||n===Number||["string","number"].includes(typeof r);Object.defineProperty(this,i,{configurable:!0,set(h){if(a)return a.call(this,o,n,r,h);l?h?this.setAttribute(o,""):this.removeAttribute(o):_&&h!=null?this.setAttribute(o,JSON.stringify(h)):R&&h!=null?this.setAttribute(o,h):this.removeAttribute(o)},get(){if(s)return s.call(this,o,n,r);if(l)return this.hasAttribute(o);if(!this.hasAttribute(o))return r;if(_)try{return JSON.parse(this.getAttribute(o))}catch{return r}if(R){let h=this.getAttribute(o);return n===Number?Number(h):h}return this.getAttribute(o)}})}function B(e,t){return{value:e,...t,name:"propReflect"}}f.define(z);function G(e,t){if(t===void 0||t?.name!="getset")return;let{value:n,forWatch:r}=t,s=`#${e}`;this[s]=n,Object.defineProperty(this,e,{configurable:!0,set(a){this[s]!==a&&(this[s]=a,r&&this.requestUpdate(e))},get(){return this[s]}})}function K(e,t){return{value:e,...{forWatch:!0},...t,name:"getset"}}f.define(G);var M=new Map,E=[],Z=":host{display:block}";function v(e,t){let n=Array.isArray(e)?e:[e],r=[];if(!t)return n.join("");for(let s of n){if(s.startsWith("<link")){let a=s.match(/href="([^"]+)"/)?.[1];if(a&&!t.querySelector(`link[href="${a}"]`)){let c=document.createElement("div");c.innerHTML=s;let i=c.firstElementChild;i&&t.appendChild(i)}continue}if("adoptedStyleSheets"in t){let a=M.get(s);a||(a=new CSSStyleSheet,a.replaceSync(s),M.set(s,a)),t.adoptedStyleSheets=[...t.adoptedStyleSheets,a];continue}r.push(`<style>${s}</style>`)}return r.join("")}function Mt(e){E.includes(e)||E.push(e)}var x=e=>Array.isArray(e)?e.map(x).join(""):e===void 0?"":String(e),J=(e,...t)=>e.reduce((n,r,s)=>n+r+x(t[s]),""),V={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"},Nt=e=>x(e).replace(/[&<>"']/g,n=>V[n]),Ut=J,N=class extends HTMLElement{_pending=!1;_changed=new Set;_connected=!1;_reactiveCache=[];_watchRegistry=new Map;_idToProps=new Map;_watchElements=new Map;_attrRegistry=new Map;_attrElements=new Map;_eventElements=new Map;styles=[];constructor(){super(),this.attachShadow({mode:"open",delegatesFocus:!0})}connectedCallback(){this._connected||(this._connected=!0,this._init())}disconnectedCallback(){$.call(this)}attributeChangedCallback(t,n,r){n!==r&&this.requestUpdate(t)}requestUpdate(t){this._changed.add(t),!this._pending&&(this._pending=!0,queueMicrotask(()=>this._flush()))}_flush(){if(this._pending=!1,!this._changed.size)return;let t=Array.from(this._changed);this._changed.clear();let n=!1;window.__redgin_current_instance=this;for(let r of t)this._update(r)&&(n=!0);window.__redgin_current_instance=null,n?this._afterUpdate():this._afterUpdateNoDomChange()}_init(){this._setupProps(),window.__redgin_current_instance=this,this.shadowRoot&&(v(E,this.shadowRoot),v(Z,this.shadowRoot),v(this.styles,this.shadowRoot),this.shadowRoot.innerHTML+=this.render()),this._collectElements(),this.onInit(),this._sync(),window.__redgin_current_instance=null}_collectElements(t="all"){if(!this.shadowRoot)return;t==="all"&&(this._watchElements.clear(),this._attrElements.clear()),this._eventElements.clear();let n=document.createTreeWalker(this.shadowRoot,NodeFilter.SHOW_ELEMENT,null),r;for(;r=n.nextNode();){let s=r.attributes;for(let a=0;a<s.length;a++){let{name:c,value:i}=s[a];c.startsWith("rg-evt__")?this._eventElements.set(i,r):t==="all"&&(c==="data-watch"?this._watchElements.set(i,r):c.startsWith("rg-attr__")&&this._attrElements.set(i,r))}}}_cleanupWatch(t){let n=this._idToProps.get(t);if(n){for(let r of n){let s=this._watchRegistry.get(r);s&&(s.delete(t),s.size||this._watchRegistry.delete(r))}this._idToProps.delete(t),this._watchElements.delete(t)}}_sync(){for(let t of this._reactiveProps())this._update(t);this._collectElements(),w.call(this),this.onDoUpdate()}_update(t){return T.call(this,t)}_afterUpdate(){this._collectElements("events"),w.call(this),this.onUpdated()}_afterUpdateNoDomChange(){this.onUpdated()}_setupProps(){if(!this._reactiveCache.length){let t=new Set(["styles","_pending","_changed","_connected"]);this._reactiveCache=Object.getOwnPropertyNames(this).filter(n=>!t.has(n))}for(let t of this._reactiveCache)P.call(this,t,this[t])}_reactiveProps(){return this._reactiveCache}onInit(){}onDoUpdate(){}onUpdated(){}render(){return""}};export{N as RedGin,v as _applyStyle,x as _f,I as attr,Ut as css,p as customDirectives,f as customPropsBehavior,Z as defaultStyle,O as emit,C as event,K as getset,J as html,L as on,B as propReflect,q as s,Nt as safe,Mt as shareStyle,E as shared,k as watch};
13
2
  //# sourceMappingURL=redgin.min.js.map
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const getUniqID: () => string;
1
+ export declare function getUniqID(): string;
2
2
  export declare const camelToKebab: (str: string) => string;
3
3
  export declare const kebabToCamel: (str: string) => string;
package/index.d.ts CHANGED
@@ -0,0 +1,20 @@
1
+ import './samples/sample-styles';
2
+ import './samples/sample-binding';
3
+ import './samples/sampleIf';
4
+ import './samples/sample-nested';
5
+ import './samples/sample-allevents';
6
+ import './samples/ecommerce-app';
7
+ import './samples/todo-catalog';
8
+ import './samples/smart-list';
9
+ import './samples/smart-update-list';
10
+ import './samples/smart-pagination-list';
11
+ import './samples/smart-sort-list';
12
+ import './samples/xss-sandbox';
13
+ import './samples/parentChild/parentChild';
14
+ import './samples/childParent/cart-manager';
15
+ import './samples/modal-app';
16
+ import './samples/master-binding';
17
+ import './samples/smart-grid';
18
+ import './samples/crm-app';
19
+ import './samples/check-search-list';
20
+ import './samples/todo-app';
package/package.json CHANGED
@@ -1,11 +1,39 @@
1
1
  {
2
2
  "name": "redgin",
3
- "version": "0.1.19",
4
- "description": "A lightweight (~5.3kb) library for building Web Components",
3
+ "version": "0.2.2",
4
+ "description": "A lightweight (~5.3kb) library for building Web Components with surgical updates and fine-grained reactivity",
5
5
  "keywords": [
6
- "redgin",
7
6
  "web components",
8
- "custom elements"
7
+ "custom elements",
8
+ "reactive",
9
+ "lightweight",
10
+ "tiny",
11
+ "reactive web components",
12
+ "custom elements library",
13
+ "web components framework",
14
+ "shadow dom",
15
+ "vanilla js",
16
+ "no dependencies",
17
+ "reactive programming",
18
+ "fine-grained reactivity",
19
+ "surgical updates",
20
+ "template literals",
21
+ "UI library",
22
+ "component library",
23
+ "frontend",
24
+ "browser",
25
+ "javascript",
26
+ "typescript",
27
+ "reactive state",
28
+ "state management",
29
+ "reactive properties",
30
+ "attribute reflection",
31
+ "custom events",
32
+ "lifecycle hooks",
33
+ "SPA",
34
+ "single page application",
35
+ "micro frontend",
36
+ "framework agnostic"
9
37
  ],
10
38
  "main": "dist/redgin.min.js",
11
39
  "types": "dist/redgin.d.ts",
@@ -19,7 +47,7 @@
19
47
  "build": "npm run clean && tsc -w",
20
48
  "bundle": "npx esbuild --bundle src/redgin.js --minify --sourcemap --format=esm --outfile=dist/redgin.min.js --target=es2022 && npm run copy",
21
49
  "copy": "cp -rvf src/*.d.ts dist/. && cp -rfv src/directives/*.d.ts dist/directives/. && cp -rfv src/props/*.d.ts dist/props/.",
22
- "dev": "node node_modules/vite/bin/vite.js"
50
+ "dev": "node node_modules/vite/bin/vite.js --host 0.0.0.0"
23
51
  },
24
52
  "repository": {
25
53
  "type": "git",
@@ -32,10 +60,10 @@
32
60
  },
33
61
  "homepage": "https://github.com/josnin/redgin#readme",
34
62
  "devDependencies": {
35
- "@types/node": "^18.11.14",
63
+ "@types/node": "^25.2.3",
36
64
  "esbuild": "^0.16.4",
37
65
  "rimraf": "^4.1.2",
38
66
  "typescript": "^4.9.5",
39
- "vite": "^4.1.1"
67
+ "vite": "^7.3.1"
40
68
  }
41
69
  }
package/vite.config.js ADDED
@@ -0,0 +1,5 @@
1
+ export default {
2
+ server: {
3
+ allowedHosts: ['myfedora.local'] // . prefix allows subdomains
4
+ }
5
+ }
@@ -1,5 +0,0 @@
1
- export declare function applyDirectives(this: any, prop: string): boolean;
2
- export declare class customDirectives {
3
- static reg: any;
4
- static define(d: any): void;
5
- }
@@ -1,4 +0,0 @@
1
- export declare const event: (type: string, fn: any) => string;
2
- export declare function emit(this: any, customEvent: string, value: any, options?: CustomEvent): void;
3
- export declare function applyEventListeners(this: any): void;
4
- export declare function removeEventListeners(this: any): void;
@@ -1,2 +0,0 @@
1
- export declare const watch: (ref: string[], exp: any) => string;
2
- export declare function watchFn(this: any, _prop: string): boolean;