Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rp contri doc #85

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<app><app/>` 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`

<!-- ## [v0.0.0] — y.m.d
[v0.0.0]: https://github.com/sidiousvic/phantom/compare/vz.z.z...v0.0.0 -->
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`

<img src="https://i.imgur.com/0o2ZFjo.gif" width="300">
<img src="https://i.imgur.com/0o2ZFjo.gif" width="250">

#### Phantom lets you build state—reactive UIs using raw HTML strings ejected from functions.

Expand Down Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
135 changes: 63 additions & 72 deletions spec/dom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `
<div>
<h1>PHANTOM</h1>
</div>
`;
}
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 `
<div>
<h1>PHANTOM</h1>
</div>
`;
}
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 `
<div id="title-div">
<h1 data-phantom="${title}" id="title-h1">${title}</h1>
</div>
`;
}
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 `
<div id="title-div">
<h1 data-phantom="${title}" id="title-h1">${title}</h1>
</div>
`;
}
}

// 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 `
<div id="title-div">
<h1 id="title-h1">${title}</h1>
</div>
`;
}

appear();

// add listener
document.addEventListener("click", justDoShit);
function justDoShit(e) {
if (e.target.id === "title-h1") {
fire({ type: "TOGGLE_TITLE" });
function phantomComponent() {
return `
<div id="phantom-test"></div>
`;
}
}

// 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);
});
});
39 changes: 39 additions & 0 deletions spec/exorciser.test.js
Original file line number Diff line number Diff line change
@@ -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("<img src='X' onerror='alert(0)'> is sanitized", () => {
// init phantom
const { appear } = phantom(phantomComponent, phantomStore);

// define a component
function phantomComponent() {
return `
<img src='X' onerror='alert(0)'>
`; // ^ dangerous <img> tag
}

const sanitized = `<img src="X">`;
const shouldBeSanitized = appear().innerHTML.trim();
// ^ appear returns the DOM node. innerHTML is trimmed for control

expect(shouldBeSanitized).toBe(sanitized);
});

test("Attempting to render <iframe> throws an DOMException", () => {
// init phantom
const { appear } = phantom(phantomComponent, phantomStore);

function phantomComponent() {
return `
<iframe>
`; // iframe is a forbidden tag
}

expect(appear).toThrowError(DOMException);
});
});
48 changes: 28 additions & 20 deletions spec/interfaces.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,36 @@ import phantomStore from "./utils/phantomStore";

/*
* Test the Phantom engine's interfaces
* > data::returns the current store state
* > fire::dispatches an action to the store
* > appear::runs the phantom engine
*/

test("the 'fire' interface points to the phantomStore dispatch", () => {
const { fire } = phantom(phantomStore, phantomElement);
function phantomElement() {
return ``;
}
expect(fire).toBe(phantomStore.fire);
});
describe("Phantom and its interfaces", () => {
test("the 'fire' interface points to the phantomStore", () => {
// init phantom
const { fire } = phantom(phantomComponent, phantomStore);

test("the 'data' interface points to the phantomStore getState", () => {
const { data } = phantom(phantomStore, phantomElement);
function phantomElement() {
return ``;
}
expect(data).toBe(phantomStore.data);
});
function phantomComponent() {
return ``;
}

expect(fire).toBe(phantomStore.fire);
});

test("the 'data' interface points to the phantomStore", () => {
const { data } = phantom(phantomComponent, phantomStore);
function phantomComponent() {
return ``;
}
expect(data).toBe(phantomStore.data);
});

test("the 'appear' interface returns an HTMLDivElement instance", () => {
const { appear } = phantom(phantomStore, phantomElement);
function phantomElement() {
return ``;
}
expect(appear() instanceof HTMLDivElement).toBe(true);
test("the 'appear' interface returns an HTMLDivElement instance", () => {
const { appear } = phantom(phantomComponent, phantomStore);
function phantomComponent() {
return ``;
}
expect(appear() instanceof HTMLDivElement).toBe(true);
});
});
40 changes: 30 additions & 10 deletions spec/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
import { PhantomAction } from "./../src/types/phantomStore";
import {
PhantomAction,
PhantomStore,
FireFunction,
} from "./../src/types/phantomStore";
import { PhantomComponent } from "./../src/types/phantom";
import phantom, { createPhantomStore } from "../src/index";

/*
* Test Phantom's type safety and exported types and interfaces
* This test is ran by the Typescript compiler
* Ignored by Jest.
*/

const initialData = {
0: "0",
zero: "0",
};

function reducer(state = initialData, action: PhantomAction) {
switch (action.type) {
case 0:
return action.payload;
default:
return state;
}
}

const phantomStore = createPhantomStore(reducer);

const { fire, data, appear } = phantom(phantomStore, phantomComponent);
const phantomStore: PhantomStore = createPhantomStore(reducer);

function phantomComponent() {
const { title } = data();
fire({ type: 0, lol: 0 });
const phantomComponent: PhantomComponent = () => {
const { zero } = data();
fire({ type: 0, payload: "test" });
return `
<div id="title-div">
<h1 data-phantom="${title}" id="title-h1">${title}</h1>
<h1 data-phantom="${zero}" id="title-h1">${zero}</h1>
</div>
`;
}
};

const {
fire,
data,
appear,
}: { fire: FireFunction; data: any; appear: () => HTMLElement } = phantom(
phantomComponent,
phantomStore
);

const appearShouldReturnHTMLElement: HTMLElement = appear();
Loading