diff --git a/CHANGELOG.md b/CHANGELOG.md index 2961875..c9bd155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,16 @@ All notable changes to [Phantom](https://github.com/sidiousvic/phantom) will be ## [Unreleased] -👻 +### 👻 v3.0.0 +- [x] Class—based +- [x] TS—friendly +- [x] Avoids innerHTML (XSS safe, no phantomExorciser) +- [x] User can define component `state` and `children` via class methods. +- [x] `render` method can define HTML as template strings, no JSX +- [x] Use custom elements such as `` to wrap component markup +- [x] Object reference to any component and its state and inner elements. `const {app, child} = Phantom(App)` +- [x] Provides access to a component's state, e.g. `app.photos` would return App's photos state. +- [x] **Experimental** potential to provide useful decorators for component methods, such as `@onMount` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 980a46f..07aec85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ ### Instructions -`0` **Mind** the [Code of Conduct](./CODEOFCONDUCT.md) +`0` **Mind** the [Code of Conduct](./CODE_OF_CONDUCT.md) `1` [**Fork** the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) on GitHub `2` [**Clone** the project](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github) to your machine `3` [**Install** dependencies](https://docs.npmjs.com/cli/install) with `npm i` diff --git a/README.md b/README.md index 16e3180..749b320 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # **Phantom** -![](https://github.com/sidiousvic/phantom/workflows/build/badge.svg) [![npm version](https://badge.fury.io/js/%40sidiousvic%2Fphantom.svg)](https://badge.fury.io/js/%40sidiousvic%2Fphantom) [![install size](https://badgen.net/packagephobia/install/@sidiousvic/phantom)](https://packagephobia.com/result?p=%40sidiousvic%2Fphantom) +![](https://github.com/sidiousvic/phantom/workflows/build/badge.svg) [![install size](https://badgen.net/packagephobia/install/@sidiousvic/phantom)](https://packagephobia.com/result?p=%40sidiousvic%2Fphantom) ### A state—reactive DOM rendering engine for building UIs. 👻 ### `npm i @sidiousvic/phantom` - + #### Phantom lets you build state—reactive UIs using raw HTML strings ejected from functions. @@ -281,7 +281,7 @@ The Phantom engine integrates with a store and subscribes to state updates. It s #### 👩🏾‍🏭 Closer to the DOM _metal_ -Frameworks often abstract too much architecture and functionality out of the DOM. They make you yield too much to _their way_ of doing things—events, effects, styling, routing—you have to find the solutions withing _their_ ecosystem. +Frameworks often abstract too much architecture and functionality out of the DOM. They make you yield too much to _their way_ of doing things—events, effects, styling, routing—you have to find the solutions within _that_ ecosystem. Phantom only helps with DOM rendering. It's convenient, but close enough to the DOM that you can integrate it with other solutions without using _fibers_, _combiners_ or _adapters_ of any kind. diff --git a/package-lock.json b/package-lock.json index 367ed5a..2de15cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10776,9 +10776,9 @@ } }, "registry-auth-token": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.1.1.tgz", - "integrity": "sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", + "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", "dev": true, "requires": { "rc": "^1.2.8" diff --git a/package.json b/package.json index 0df02b6..ad8d947 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,16 @@ } }, "scripts": { - "clean": "rimraf lib dist es types", + "clean": "rimraf lib dist es types x", "build": "rollup -c", "preversion": "npm test", "postversion": "git push origin --tags --no-verify", - "test": "jest && tsc spec/types.test.ts --noEmit", + "test": "jest && npm run test-types", + "test-types": "tsc spec/types.test.ts --noEmit", "example/pizza": "webpack --config examples/pizza/webpack.config.js && webpack-dev-server --mode development --hot --watch-stdin --config examples/pizza/webpack.config.js", "example/todo": "webpack --config examples/todo/webpack.config.js && webpack-dev-server --mode development --hot --watch-stdin --config examples/todo/webpack.config.js", "example/calculator": "webpack --config examples/calculator/webpack.config.js && webpack-dev-server --mode development --hot --watch-stdin --config examples/calculator/webpack.config.js", + "x": "webpack --config x/webpack.config.js && webpack-dev-server --mode development --hot --watch-stdin --config x/webpack.config.js", "pretest": "npm run build" }, "keywords": [ diff --git a/spec/dom.test.js b/spec/dom.test.js index 7b88865..6fe25b1 100644 --- a/spec/dom.test.js +++ b/spec/dom.test.js @@ -2,88 +2,79 @@ import phantom from "../lib/phantom"; import phantomStore from "./utils/phantomStore"; /* - * Test Phantom's DOM output + * Test Phantom and its interactions with the DOM */ -test("The PHANTOM element is rendered and wraps around the application", () => { - // init phantomElement - const { appear } = phantom(phantomStore, phantomElement); - function phantomElement() { - return ` -
-

PHANTOM

-
- `; - } - appear(); - - const PHANTOMEl = document.body.firstChild; - - expect(PHANTOMEl.id).toBe("PHANTOM"); -}); +describe("Phantom and the DOM", () => { + test("The PHANTOM element is rendered and wraps around the application", () => { + // init phantomComponent + const { appear } = phantom(phantomComponent, phantomStore); + function phantomComponent() { + return ` +
+

PHANTOM

+
+ `; + } + appear(); + + const PHANTOMEl = document.body.firstChild; -test("DOM is updated after firing a state change", () => { - // init phantomElement - const { fire, data, appear } = phantom(phantomStore, phantomElement); - function phantomElement() { - const { title } = data(); - return ` -
-

${title}

-
- `; - } - appear(); - - // add listener - document.addEventListener("click", justDoShit); - function justDoShit(e) { - if (e.target.id === "title-h1") { - fire({ type: "TOGGLE_TITLE" }); + expect(PHANTOMEl.id).toBe("PHANTOM"); + }); + + test("DOM is updated after firing a state change", () => { + // init phantomComponent + const { fire, data, appear } = phantom(phantomComponent, phantomStore); + function phantomComponent() { + const { title } = data(); + return ` +
+

${title}

+
+ `; } - } - // click title element - const toBeSwappedOut = document.getElementById("title-h1"); - toBeSwappedOut.click(); - const swappedIn = document.getElementById("title-h1"); + appear(); - expect(swappedIn.innerHTML).toBe("JUST DO SHIT"); -}); + // add click listener + document.addEventListener("click", justDoShit); + function justDoShit(e) { + if (e.target.id === "title-h1") { + fire({ type: "TOGGLE_TITLE" }); + } + } + + const toBeSwappedOut = document.getElementById("title-h1"); + + // simulate click + toBeSwappedOut.click(); + + const swappedIn = document.getElementById("title-h1"); + + expect(swappedIn.innerHTML).toBe("JUST DO SHIT"); + }); + + test("PHANTOM element is properly rendered", () => { + // init phantomComponent + const { data, appear } = phantom(phantomComponent, phantomStore); -test("PHANTOM DOM is properly rendered", () => { - // init phantomElement - const { fire, data, appear } = phantom(phantomStore, phantomElement); - function phantomElement() { - const { title } = data(); - return ` -
-

${title}

-
- `; - } - - appear(); - - // add listener - document.addEventListener("click", justDoShit); - function justDoShit(e) { - if (e.target.id === "title-h1") { - fire({ type: "TOGGLE_TITLE" }); + function phantomComponent() { + return ` +
+ `; } - } - // get dom element's innerHTML, trim() - const domElementInnerHTML = document - .getElementById("PHANTOM") - .innerHTML.trim(); + appear(); - // click title element - const titleH1 = document.getElementById("title-h1"); - titleH1.click(); + // get dom element's innerHTML, trim() + const domElementInnerHTML = document + .getElementById("PHANTOM") + .innerHTML.trim(); - // get phantomElement's (as returned by phantomElement) HTML, trim() - const phantomElementHTML = phantomElement().trim(); + // get phantomComponent's HTML string, trim() + const phantomElementHTML = phantomComponent().trim(); - expect(domElementInnerHTML).toBe(phantomElementHTML); + expect(domElementInnerHTML).toBe(phantomElementHTML); + }); }); diff --git a/spec/exorciser.test.js b/spec/exorciser.test.js new file mode 100644 index 0000000..d3c3849 --- /dev/null +++ b/spec/exorciser.test.js @@ -0,0 +1,39 @@ +import phantom from "../lib/phantom"; +import phantomStore from "./utils/phantomStore"; + +/* + * Test the internal Phantom Exorciser and its HTML sanitization vs XSS injection + */ + +describe("The Phantom Exorciser", () => { + test(" is sanitized", () => { + // init phantom + const { appear } = phantom(phantomComponent, phantomStore); + + // define a component + function phantomComponent() { + return ` + + `; // ^ dangerous tag + } + + const sanitized = ``; + const shouldBeSanitized = appear().innerHTML.trim(); + // ^ appear returns the DOM node. innerHTML is trimmed for control + + expect(shouldBeSanitized).toBe(sanitized); + }); + + test("Attempting to render