Skip to content

Commit 4ae8644

Browse files
committed
added callback to handle cleanups
1 parent b90836e commit 4ae8644

11 files changed

Lines changed: 246 additions & 9 deletions

File tree

build/vite-plugin-openscript.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,15 @@ export function openScriptComponentPlugin(options = {}) {
203203
if (hasChanged) {
204204
// Check if Component is imported
205205
// Matches: import { Component } ... or import ... Component ...
206+
// We use a regex that supports multi-line named imports by looking inside braces
207+
208+
const hasNamedImport =
209+
/import\s*\{[^}]*?\bComponent\b[^}]*?\}\s*from/.test(code);
210+
const hasDefaultOrNamespaceImport =
211+
/import\s+(?:[\w*\s,]*\bComponent\b)/.test(code);
212+
206213
// Simple check:
207-
if (!code.includes("import") || !code.match(/import\s+.*Component/)) {
214+
if (!hasNamedImport && !hasDefaultOrNamespaceImport) {
208215
// Need to import Component.
209216
// Check if existing import from "modular-openscriptjs" exists
210217
if (code.includes("modular-openscriptjs")) {

docs/components.md

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,18 @@ OpenScript provides a declarative way to listen to events on elements.
7878

7979
When creating an element with `h`, you can pass a `listeners` object in the attributes.
8080

81+
> [!TIP]
82+
> It is recommended to use **anonymous functions** for listeners to avoid potential memory leaks associated with direct binding. While OpenScript is generally memory-safe, using anonymous functions ensures that references are properly managed and collected.
83+
8184
```javascript
8285
h.button(
8386
{
8487
class: "btn",
8588
listeners: {
86-
click: this.handleClick.bind(this),
89+
// Preferred: Anonymous function
90+
click: (e) => this.handleClick(e),
91+
92+
// Also safe: Arrow functions defined inline
8793
mouseover: (e) => console.log("Hovered", e),
8894
},
8995
},
@@ -93,7 +99,7 @@ h.button(
9399

94100
### Method Binding
95101

96-
For class components, it's common to define methods for event handlers. Remember to `.bind(this)` or use arrow functions to preserve the correct `this` context.
102+
While you can bind methods directly, be aware that creating new bound functions (e.g., `.bind(this)`) on every render can potentially lead to memory overhead if not handled correctly by the garbage collector.
97103

98104
```javascript
99105
export default class Counter extends Component {
@@ -107,7 +113,8 @@ export default class Counter extends Component {
107113
return h.button(
108114
{
109115
listeners: {
110-
click: this.increment.bind(this), // Binding is crucial
116+
// Anonymous function wrapper is preferred over .bind(this)
117+
click: () => this.increment(),
111118
},
112119
},
113120
"+",
@@ -118,7 +125,59 @@ export default class Counter extends Component {
118125

119126
### Special Event Methods
120127

121-
If your component class defines methods starting with `$_`, OpenScript automatically treats them as event listeners for the component instance itself (lifecycle events).
128+
OpenScript provides conventions for automatically listening to events based on method names.
129+
130+
#### Component Lifecycle & Emitted Events (`$_`)
131+
132+
Methods starting with `$_` are treated as listeners for events emitted by the component itself (including lifecycle events).
122133

123134
- `$_mounted()`: Called when the component is added to the DOM.
124135
- `$_rendered()`: Called when the component is rendered.
136+
- `$_customEvent()`: Listens for `this.emit('customEvent')`.
137+
138+
#### Broker Events (`$$`)
139+
140+
Methods starting with `$$` are treated as listeners for global events emitted via the **Broker**.
141+
142+
- `$$app_started()`: Listens for `app:started` event (dots/colons usually mapped to underscores).
143+
- `$$user_login()`: Listens for `user:login` event.
144+
145+
```javascript
146+
export default class UserProfile extends Component {
147+
// Listen to component's own mount event
148+
$_mounted() {
149+
console.log("UserProfile mounted");
150+
}
151+
152+
// Listen to global 'auth:logout' event from Broker
153+
$$auth_logout(user) {
154+
console.log("User logged out:", user);
155+
this.cleanUp();
156+
}
157+
}
158+
```
159+
160+
### Inline Attribute Listeners
161+
162+
For inline event attributes (like `onclick`, `onchange`, etc.) that mimic standard HTML attributes, you can use `this.method('methodName', ...args)`. This approach allows you to reference component methods directly in the string attribute, which is useful when standard `listeners` object binding isn't applicable or preferred for specific attribute-based APIs.
163+
164+
```javascript
165+
export default class MyComponent extends Component {
166+
greet(name) {
167+
alert(`Hello, ${name}!`);
168+
}
169+
170+
render() {
171+
// Uses this.method to create a reference to the 'greet' method
172+
// 'onclick' here is treated as an attribute, not a direct event listener attachment
173+
return h.button(
174+
{
175+
onclick: this.method("greet", "Levi"), // effectively onclick="...greet('Levi')"
176+
},
177+
"Say Hello",
178+
);
179+
}
180+
}
181+
```
182+
183+
_Note: `this.method()` is specifically for attributes that expect a string script (like `onclick` in HTML), bridging them back to your component's methods._

docs/osm.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# OpenScript Markup (OSM)
2+
3+
OpenScript Markup (OSM) is a powerful, JavaScript-based Domain Specific Language (DSL) for generating HTML. It uses the `h` object, a proxy that translates methods into HTML elements.
4+
5+
## Basic Syntax
6+
7+
To use OSM, you need to import the `app` instance and retrieve the `h` service.
8+
9+
```javascript
10+
import { app } from "modular-openscriptjs";
11+
12+
const h = app("h");
13+
14+
// Simple element
15+
const div = h.div("Hello World");
16+
// Output: <div>Hello World</div>
17+
```
18+
19+
### How it Works
20+
21+
The `h` object is a **Proxy**. When you access a property on it (e.g., `h.div`, `h.span`, `h.customElement`), it returns a function that generates an element with that tag name.
22+
23+
> **Note**: OSM supports all standard HTML tags (`h.section`, `h.a`, `h.img`) and custom elements (`h.myComponent`).
24+
25+
## Attributes & Properties
26+
27+
You can pass attributes as the first argument to the tag function if it is an object (and not a DOM node or State).
28+
29+
```javascript
30+
h.a(
31+
{
32+
href: "https://example.com",
33+
class: "link primary",
34+
target: "_blank",
35+
id: "main-link",
36+
},
37+
"Visit Example",
38+
);
39+
```
40+
41+
### Boolean Attributes
42+
43+
Boolean attributes work as expected:
44+
45+
```javascript
46+
h.input({ type: "checkbox", checked: true, disabled: false });
47+
```
48+
49+
### Style Object
50+
51+
You can pass a style string directly:
52+
53+
```javascript
54+
h.div({ style: "color: red; font-weight: bold;" }, "Styled Text");
55+
```
56+
57+
## Children & Text Content
58+
59+
Arguments after the attributes (or the first argument if it's not an attributes object) are treated as children.
60+
61+
```javascript
62+
h.ul(
63+
{ class: "list" },
64+
h.li("Item 1"),
65+
h.li("Item 2"),
66+
h.li(h.strong("Item 3 with bold text")),
67+
);
68+
```
69+
70+
### Text Nodes
71+
72+
Strings and numbers are automatically converted to text nodes.
73+
74+
```javascript
75+
h.p("You have ", 5, " notifications.");
76+
```

docs/setting-up.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,26 @@ export function configureApp() {
170170
* ---------------------------------------------
171171
*/
172172
app().value("appEvents", appEvents);
173+
174+
/**
175+
* ---------------------------------------------
176+
* Node Disposal Callback
177+
* ---------------------------------------------
178+
* Use this to clean up external library instances
179+
* attached to DOM nodes when they are removed.
180+
*/
181+
registerNodeDisposalCallback((node) => {
182+
// Example: Dispose Bootstrap tooltips/popovers
183+
// if (node._bootstrap_tooltip) node._bootstrap_tooltip.dispose();
184+
});
173185
}
174186

175187
// execute configuration
176188
configureApp();
177189
```
178190

191+
> **Note**: `registerNodeDisposalCallback` is crucial for preventing memory leaks when using third-party libraries that attach instances to DOM elements (like Bootstrap, Tippy.js, etc.). The callback **MUST** be synchronous and stateless.
192+
179193
> **Note**: In the configuration above, we are using `appEvents` imported from `events.js`. We will cover the creation of `events.js` and how to handle events in the subsequent sections.
180194
181195
## 5. Define Application Events

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "modular-openscriptjs",
3-
"version": "2.0.9",
3+
"version": "2.0.12",
44
"description": "OpenScriptJs Framework - A lightweight, reactive JavaScript framework for building modern web applications",
55
"type": "module",
66
"main": "./dist/modular-openscriptjs.umd.js",

src/component/Component.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export default class Component {
8484

8585
this.isAnonymous = false;
8686

87-
8887
this.emitter.once(this.EVENTS.rendered, (componentId) => {
8988
let repo = container.resolve("repository");
9089
let component = repo.findComponent(componentId);

src/core/Container.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,42 @@ export default class Container {
8686
});
8787
return this;
8888
}
89-
89+
/**
90+
* Access services from the IoC container
91+
* @overload
92+
* @param {'h'} instance - Get the MarkupEngine instance
93+
* @returns {MarkupEngine}
94+
*/
95+
/**
96+
* @overload
97+
* @param {'repository'} instance - Get the Repository instance
98+
* @returns {Repository}
99+
*/
100+
/**
101+
* @overload
102+
* @param {'router'} instance - Get the Router instance
103+
* @returns {Router}
104+
*/
105+
/**
106+
* @overload
107+
* @param {'broker'} instance - Get the Broker instance
108+
* @returns {Broker}
109+
*/
110+
/**
111+
* @overload
112+
* @param {'contextProvider'} instance - Get the ContextProvider instance
113+
* @returns {ContextProvider}
114+
*/
115+
/**
116+
* @overload
117+
* @param {'mediatorManager'} instance - Get the MediatorManager instance
118+
* @returns {MediatorManager}
119+
*/
120+
/**
121+
* @overload
122+
* @param {'repository'} instance - Get the Repository instance
123+
* @returns {Repository}
124+
*/
90125
/**
91126
* Resolve a service by name
92127
* @param {string} name - Service identifier

src/core/Repository.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export default class Repository {
3636

3737
this.domListeners = new WeakMap();
3838
this.domMethods = new WeakMap();
39+
40+
this.nodeDisposalCallbacks = new Set();
3941
}
4042

4143
/**
@@ -156,4 +158,12 @@ export default class Repository {
156158
if (!component) return;
157159
this.componentArgsMap.delete(component);
158160
}
161+
162+
/**
163+
* Get the node disposal callbacks
164+
* @returns {Set<Function>}
165+
*/
166+
getNodeDisposalCallbacks() {
167+
return this.nodeDisposalCallbacks;
168+
}
159169
}

src/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import MarkupHandler from "./component/MarkupHandler.js";
2323

2424
import Utils from "./utils/Utils.js";
2525
import DOM from "./utils/DOM.js";
26-
import { cleanUpNode, isClass } from "./utils/helpers.js";
26+
import { cleanUpNode, isClass, registerNodeDisposalCallback, removeNode } from "./utils/helpers.js";
2727

2828
// Initialize global instances
2929
const broker = new Broker();
@@ -191,6 +191,8 @@ export {
191191
payload,
192192
ojsRouterEvents,
193193
removeNodeModifications,
194+
removeNode,
195+
registerNodeDisposalCallback
194196
};
195197

196198
// Default export object
@@ -233,6 +235,8 @@ export default {
233235
payload,
234236
ojsRouterEvents,
235237
removeNodeModifications,
238+
removeNode,
239+
registerNodeDisposalCallback
236240
};
237241

238242
// Add necessary globals

src/utils/helpers.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export function destroyNodeDeep(node) {
7171
destroyNodeDeep(child);
7272
}
7373

74+
if(container.resolve("repository")?.getNodeDisposalCallbacks()?.size) {
75+
for (const callback of container.resolve("repository").getNodeDisposalCallbacks()) {
76+
callback(node);
77+
}
78+
}
79+
7480
cleanUpNode(node);
7581
}
7682

@@ -105,3 +111,24 @@ export function registerDomListeners(node, event, listener) {
105111
listeners.add(listener);
106112
eventMap.set(event, listeners);
107113
}
114+
115+
/**
116+
* used to safely remove a node from the DOM
117+
* @param {Node} node
118+
*/
119+
export function removeNode(node) {
120+
destroyNodeDeep(node);
121+
node.remove();
122+
}
123+
124+
/**
125+
* used to register a callback that will be called when a node is removed from the DOM. Use this to clean up the node to avoid memory leaks. e.g. Remove Bootstrap components attached to the node.
126+
* **The Callback Must Be Stateless!**
127+
* **It must not be asynchronous!**
128+
* **If you don't understand, GOOGLE IT!**
129+
* @param {(node: Node) => void} syncStatelessCallback
130+
* @returns {void}
131+
*/
132+
export function registerNodeDisposalCallback(syncStatelessCallback) {
133+
container.resolve("repository").nodeDisposalCallbacks?.add(syncStatelessCallback);
134+
}

0 commit comments

Comments
 (0)