diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..76add87
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+dist
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ea3c971
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+# What Is This?
+This is a super simple signal library for browser and Node. It's mostly an experiment which provided good benchmarks and a nice, clean and simple API.
+
+# Installation
+```bash
+npm install super-simple-signal
+```
+
+# Usage
+```html
+
+
+
+
+
+```
+
+# API
+## Signal
+### `new Signal(value)`
+Creates a new signal with the given initial value.
+
+### `new Signal(node)`
+Creates a new signal which is attached to the given node.
+
+### `new Signal(node, value)`
+Creates a new signal which is attached to the given node and has the given initial value.
+
+### `new Signal(node, value, property)`
+Creates a new signal which is attached to the given node and has the given initial value. The signal will update the given property of the node. Defaults to `innerHTML`.
+
+### `signal.value`
+The current value of the signal.
+
+### `signal.subscribe(callback)`
+Subscribes the given callback to the signal. The callback will be called whenever the signal's value changes. The callback will be called with the new value as the first argument.
+
+### `signal.unsubscribe(callback)`
+Unsubscribes the given callback from the signal.
+
+### `signal.attachTo(node, property)`
+Attaches the signal to the given node. The signal will update the node's given property whenever the signal's value changes. Defaults to `innerHTML`.
+
+### `signal.detachFrom(node)`
+Detaches the signal from the given node.
+
+### `signal.copyTo(node, property, keepInSync)`
+Copies the signal's value to the given node. This will actually create a new `Signal`. If `keepInSync` is `true`, the new signal will be updated whenever the initial signal changes. Defaults to `false`.
+
+
+
+# Considerations
+- Currently the library only supports one-way binding. Updating the signal will update the DOM but not the other way around.
+
+# ToDo
+- [ ] Add tests
+- [ ] Two-way binding
\ No newline at end of file
diff --git a/examples/attachTo.html b/examples/attachTo.html
new file mode 100644
index 0000000..d9a570d
--- /dev/null
+++ b/examples/attachTo.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Signals Demo
+
+
+
+
+
+
+
+
diff --git a/examples/benchmark.html b/examples/benchmark.html
new file mode 100644
index 0000000..6bc3c74
--- /dev/null
+++ b/examples/benchmark.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ Signals Demo
+
+
+
+
+
+
+
+
+
diff --git a/examples/btn.html b/examples/btn.html
new file mode 100644
index 0000000..65ed3ea
--- /dev/null
+++ b/examples/btn.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Signals Demo
+
+
+
+
+
+
+
+
diff --git a/examples/computed.html b/examples/computed.html
new file mode 100644
index 0000000..568beeb
--- /dev/null
+++ b/examples/computed.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Signals Demo
+
+
+
+
+
+
+
+
diff --git a/examples/copyTo.html b/examples/copyTo.html
new file mode 100644
index 0000000..ca6cdcf
--- /dev/null
+++ b/examples/copyTo.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Signals Demo
+
+
+
+
+
+
+
+
diff --git a/examples/subscriber.html b/examples/subscriber.html
new file mode 100644
index 0000000..0b0b0da
--- /dev/null
+++ b/examples/subscriber.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Signals Demo
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..9750de8
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,84 @@
+{
+ "name": "super-simple-signal",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "super-simple-signal",
+ "version": "1.0.0",
+ "license": "ISC",
+ "devDependencies": {
+ "rollup": "^3.18.0",
+ "typescript": "^4.9.5"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.18.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.18.0.tgz",
+ "integrity": "sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ }
+ },
+ "dependencies": {
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "rollup": {
+ "version": "3.18.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.18.0.tgz",
+ "integrity": "sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4aaeb02
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "super-simple-signal",
+ "version": "1.0.0",
+ "description": "A simple signal implementation in TypeScript for the browser and Node.",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "start": "tsc -w",
+ "build": "rm -rf ./dist/* && tsc && rollup ./dist/tmp/index.js --file ./dist/index.js --format iife --name SuperSimpleSignal --sourcemap && rm -rf ./dist/tmp",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [
+ "signal",
+ "typescript",
+ "browser",
+ "node",
+ "event",
+ "event emitter",
+ "event dispatcher",
+ "event bus",
+ "event system",
+ "event manager",
+ "event handler",
+ "event listener",
+ "event binding"
+ ],
+ "author": "Michael Di Prisco ",
+ "license": "ISC",
+ "devDependencies": {
+ "rollup": "^3.18.0",
+ "typescript": "^4.9.5"
+ }
+}
diff --git a/src/Computed.ts b/src/Computed.ts
new file mode 100644
index 0000000..130838c
--- /dev/null
+++ b/src/Computed.ts
@@ -0,0 +1,52 @@
+import Signal from "./Signal";
+
+// Computed is a signal that is derived from other signals and is updated when any of its dependencies change.
+declare class Computed extends Signal {
+ /** @internal */
+ _dependencies: Array;
+
+ /** @internal */
+ _fn: (...args: any[]) => T;
+
+ constructor(fn: () => T, dependencies: Signal[]);
+
+ /** @internal */
+ _set(): void;
+}
+
+function Computed(this: Computed, dependencies: Signal | Signal[], fn: (...args: any[]) => any, node: Node | undefined = undefined, value: unknown = undefined, property: string | undefined = "innerHTML") {
+ dependencies = Array.isArray(dependencies) ? dependencies : [dependencies];
+ if (!(this instanceof Computed)) {
+ return new Computed(fn, dependencies);
+ }
+
+ this._dependencies = dependencies;
+ this._fn = fn;
+
+ for (const dependency of dependencies) {
+ dependency.subscribe(this._set.bind(this));
+ }
+
+ this._set();
+}
+
+Computed.prototype = Object.create(Signal.prototype);
+Computed.prototype.constructor = Computed;
+
+Computed.prototype._set = function () {
+ this._oldValue = this._value;
+ this._value = this._fn(this._dependencies.length > 1 ? this._dependencies.map(dependency => dependency.value) : this._dependencies[0].value, this._dependencies.length > 1 ? this._dependencies.map(dependency => dependency._oldValue) : this._dependencies[0]._oldValue);
+ this._version++;
+ this._listeners.forEach(listener => listener(this._value, this._oldValue));
+ this._refresh();
+};
+
+Object.defineProperty(Computed.prototype, "value", {
+ set: function (value) {
+ throw new Error("Computed signals cannot be set");
+ },
+ get: function () {
+ return this._value;
+ },
+});
+export default Computed;
diff --git a/src/Signal.ts b/src/Signal.ts
new file mode 100644
index 0000000..701f1b3
--- /dev/null
+++ b/src/Signal.ts
@@ -0,0 +1,134 @@
+declare class Signal {
+ /** @internal */
+ _value: unknown;
+
+ /** @internal */
+ _oldValue: unknown;
+
+ /**
+ * @internal
+ */
+ _version: number;
+
+ /** @internal */
+ _node?: Node;
+
+ /** @internal */
+ _property: string;
+
+ /** @internal */
+ _listeners: Set<(...args: unknown[]) => void>;
+
+ constructor(node?: Node | undefined, value?: T, property?: string);
+ constructor(value?: T, node?: Node | undefined, property?: string);
+
+ /** @internal */
+ _refresh(): boolean;
+
+ subscribe(fn: (value: T) => void): () => void;
+
+ unsubscribe(fn: (value: T) => void): void;
+
+ attachTo(node: Node, property?: string): void;
+
+ copyTo(node: Node, property?: string, keepInSync?: boolean): Signal;
+
+ detachFrom(node: Node): void;
+
+ toString(): string;
+
+ get value(): T;
+ set value(value: T);
+}
+
+function Signal(this: Signal, node: Node | undefined = undefined, value: unknown = undefined, property: string = "innerHTML") {
+ if (!(this instanceof Signal)) {
+ return new Signal(node, value, property);
+ }
+
+ if (typeof node !== "object") {
+ // We can assume that the first argument is the value
+ value = node;
+ node = undefined;
+ }
+
+ this._value = value;
+ this._oldValue = undefined;
+ this._version = 0;
+ this._node = node;
+ this._property = property;
+ this._listeners = new Set();
+
+ if (node && value) {
+ node[property] = value;
+ } else if (node && !value) {
+ this._value = node[property];
+ }
+ // else if (!node && !value) {
+ // throw new Error("Signal must be initialized with a value or a node");
+ // }
+}
+
+Signal.prototype = {
+ _value: undefined,
+ _oldValue: undefined,
+ _version: 0,
+ _node: undefined,
+ _property: "innerHTML",
+ _listeners: new Set(),
+
+ _refresh() {
+ if (this._node) {
+ this._node[this._property] = this._value;
+ return true;
+ }
+ return false;
+ },
+
+ subscribe(fn: (value: unknown) => void) {
+ this._listeners.add(fn);
+ return () => this.unsubscribe(fn);
+ },
+
+ unsubscribe(fn: (value: unknown) => void) {
+ this._listeners.delete(fn);
+ },
+
+ attachTo(node: Node, property: string) {
+ property = property || this._property;
+ this._node = node;
+ node[property] = this._value;
+ },
+
+ copyTo(node: Node, property: string, keepInSync: boolean = false) {
+ property = property || this._property;
+ const signal = new Signal(node, this._value, property);
+ // To prevent circular updates, we only subscribe to the signal if keepInSync is true and don't subscribe back.
+ if (keepInSync) this.subscribe(value => (signal.value = value));
+ return signal;
+ },
+
+ detachFrom(node: Node) {
+ if (this._node === node) {
+ this._node = undefined;
+ }
+ },
+
+ toString() {
+ return String(this._value);
+ },
+
+ get value() {
+ return this._value;
+ },
+
+ set value(value: unknown) {
+ this._oldValue = this._value;
+ this._value = value;
+ this._version += 1;
+ this._listeners.forEach(fn => fn(value, this._oldValue));
+ this._refresh();
+ },
+};
+
+export default Signal;
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..9bbdd2f
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,12 @@
+import Signal from "./Signal";
+import Computed from "./Computed";
+
+declare global {
+ interface Window {
+ Signal: typeof Signal;
+ Computed: typeof Computed;
+ }
+}
+
+window.Signal = Signal;
+window.Computed = Computed;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0c2d430
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "module": "es6",
+ "lib": ["es2015", "dom"],
+ "outDir": "./dist/tmp",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "declaration": true,
+ "strict": true,
+ "noImplicitAny": false
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"]
+}