@xipjs/xip
v0.2.3
Published
Xip is a performant UI framework with declarative components and flexible state.
Downloads
9
Readme
Xip
Xip is a performant UI framework with declarative components and flexible state.
Features
- Surgical updates with minimal overhead
- Powerful extension api
- Simple and flexible state management
- Only 1.6 kB gzip
Contents
- Install
- Counter Example
- URL Routing
- Reactivity and State Management
- Functions
- Element Attributes
- Direct Element Access
- Dom Events
- Behaviour and Optimisation
- Intercept Element Removal
- In HTML
- Syntax
- Contact
- Licence
Install
⚡Quick Start
- Initialize the project:
npm create xip "MyApp"
- Enter project directory
cd MyApp
- Install:
npm install
- Start Live Development Server:
npm run dev
- Build:
npm run build
📦 NPM
npm i @xipjs/xip
📮CDN
<script src="https://www.unpkg.com/@xipjs/[email protected]/min/xip.gt.min.js"></script>
See In HTML for more info
Counter Example
- Define the "Counter" component
function Counter() {
// define the count state
let count = ref(0);
return (
<span>
{/* tell the h1 to react to the count state */}
<h1 react={count.Reg}>{() => count.value}</h1>
{/* add buttons to update the count causing the h1 to update */}
<button onClick={() => count.Set(count.value + 1)}>+</button>
<button onClick={() => count.Set(count.value - 1)}>-</button>
<hr />
</span>
);
}
- Add the "Counter" component to the dom
Engage(Counter, (el) => document.body.replaceChildren(el));
URL Routing
An example of no-reload url routing:
App.jsx
import Docs from "./Components/Docs/Docs";
import Examples from "./Components/Examples/Examples";
import Home from "./Components/Home/Home";
import TopBar from "./Components/NavBar/NavBar";
import { State } from "./State";
export default function App() {
return (
<div class="App">
<NavBar />
<Router />
</div>
);
}
NavBar.jsx
import { State } from "../../State";
const Redirect = (path) => () => State.Path.Set(path);
export default function NavBar() {
return (
<nav>
<a onClick={Redirect("/Home")}>Home</a>
<a onClick={Redirect("/Docs")}>Docs</a>
<a onClick={Redirect("/Examples")}>Examples</a>
</nav>
);
}
Router.jsx
function Router() {
return (
<span react={State.Path.Reg}>
{() => {
switch (State.Path.value) {
case "/Home":
return <Home />;
case "/Docs":
return <Docs />;
case "/Examples":
return <Examples />;
default:
// If path is not available redirect to /Home
State.Path.Set("/Home");
return;
}
}}
</span>
);
}
State.js
// Define a Global State object
export const State = {
// Set the initial State to the URL path
Path: ref(window.location.pathname),
// just for nicer components
Redirect(path) => () => State.Path.Set(path);
};
// Add a subscriber to update the url when "State.Path" changes
State.Path.Reg(() => {
let l = State.Path.value.split("/");
// Update URL Without reloading the page
window.history.pushState("", l[l.length - 1], State.Path.value);
});
Reactivity and State Management
State can be defined anywhere and in any form (e.g: class, object, variable). To make a value reactive, wrap it in the ref() function:
// non reactive
let count = 0
// reactive
let count = ref(0)
// use the value of count
let component = <div>{count.value}</div>
By default nothing will react to "count".
Make the component react to count: ("react={count.Reg}")
let component = <div react={count.Reg}>{count.value}</div>
Now the component will check for possible update when "count" is triggered. Even if the value of "count" has changed, the renderer has already evaluated "count.value", so no changes will be made to the dom.
Wrapping the value with a closure tells the renderer to re-evaulate and update the asosiated dom node when asked to rerender:
let component = <div react={count.Reg}>{()=>count.value}</div>
Any type of child can be wrapped in a closure
Reactive Attributes
className
If className is wrapped in a closure it becomes reactive:
let class = ref("")
<div className={()=>class.value} react={class.reg}></div>
When "class" is triggered, the renderer will directly update the "class" attribute of the dom element without making any other changes to the dom.
Functions
Engage()
function Engage(
Ui: () => CDN,
cb: (e: Element | any, r: () => void) => void
): void {}
Usage:
The first parameter is the Component to be rendered and the second is a function adding the component to the dom
An object Represinting the already attached element will be returned.
const component = Engage(App, (element) => document.body.replaceChildren(element));
component.Remove()
ref()
function ref<Type>(ival: Type): refHook<Type>;
Usage:
// define count with an initial value of 0
let count = ref(0)
// Call listeners without updating the value
count.Trigger()
// Set a new value and call listeners
count.Set(1)
// update the last value and call listeners
count.update(v => v+1)
// adds a listener and returns a function to remove the listener
let remove = count.Reg(()=>console.log("count updated"))
// Define a state anywhere with any value
let Gallary = {
Loaded: ref(false),
images: ref([]),
SearchQuery: ref("")
// Register a ui component to update in response to count being updated
Ui: <div react={count.Reg}>Loaded {Count.Value} Images</div>
}
// Register for state changes
Gallary.Loaded.Reg(() => console.log("Images Loaded"));
Gallary.Images.Reg(() => count.Set(Gallary.Images.value.Length));
// Only Subscribe once
let cancel = Gallary.Images.Reg(() => {console.log("First Image Loaded");cancel()});
el()
function el(
type: string,
Attributes: ElementAttributes,
...Children: any[]
): ElementDomNode;
Usage:
// used to define html elements
let Component = el("div", { class: "container" });
// optional children
let Component = el("div", { class: "container" }, "hello world");
// unlimited children
let Component = el("div", { class: "container" }, "hello world", el("div", {}));
// Nested
let Component = el(
"div",
{ class: "container" },
el(
"span",
{ id: "1" },
el("button", { onClick: () => console.log("pressed") }, "press")
)
);
Note: el()
can be called using jsx, for example:
<div class="container">container<div/>
will call el()
like this:
el("div", { class: "container" }, "container");
see Syntax
Element Attributes
export interface XipElementAttributes {
// Selectors
id?: string; // html id
class?: string; // html class
className?: string | (() => string); // Reactive class
// Inline Style
style?: string; //html style
Style?: CSSStyleDeclaration;
reStyle?: (style: CSSStyleDeclaration) => void;
// Render Stages
withRender?: (e?: Element) => void; // While the component is being built
onDom?: (e?: Element) => void; // As soon as the component is added to the dom
onRemove?: (e?: Element) => Promise<any>; // Before the component leaves the dom
// Dom Events
on?:
| [EventName: string, CallBack: (event?: Event) => void] // calls element.addEventListener(EventName, CallBack);
| [[EventName: string, CallBack: (event?: Event) => void]]; // use a 2d arry to add multiple event listener's
onClick?: (event?: Event) => void;
onInput?: (event?: Event) => void;
onSubmit?: (event?: Event) => void;
onContextMenu?: (event?: Event) => void;
// Other
x?: (api: EAPI) => void | [(api: EAPI) => void]; // pass 1, or an array of extensions
focus?: boolean; //
getRemover?: (remove: () => void) => void; // receive a callback to safely remove the element
HotProps?: () => ElementAttributes; // Runs when the element is told to Re-render and directly applies the new attributes without re-rendering
react?:
| ((func: () => void) => () => void) // Receive a re-render function and return a cleanup function
| [(func: () => void) => () => void]; // Register multiple with an array
[key: string]: any; // add attributes to the dom element
}
Direct Element Access
Three of the Element Attributes provide direct access to the dom element at different stages:
withRender()
while being createdonDom()
once added to the domonRemove()
before the element is removed Each of these functions are provided with the same dom element pointer. For access outside of the element usewithRender()
for predisplay delivery:
import { State } from "../../State";
var CurrentFormPointer;
const MakeFormRed = () => {
CurrentFormPointer.style.color = "red";
};
export default function NavBar() {
return (
<form withRender={(e) => (CurrentFormPointer = e)}>
<button onClick={makeFormRed}>Make Form Red</button>
</form>
);
}
Dom Events
For events not defined in [[#Element Attributes]] use the on
attribute:
<div on={["click", (e) => console.log(e)]}></div>
and use an array for multiple events on a single element:
<div
on={[
["click", (e) => console.log(e)],
["mouseover", (e) => console.log(e)],
]}
></div>
In HTML
JSX cannot be interpreted by a browser, so use HyperScript syntax instead.
Counter Example
Paste this into index.html
and open in a browser to test
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Counter</title>
</head>
<body></body>
<script src="https://www.unpkg.com/@xipjs/[email protected]/min/xip.gt.min.js"></script>
<script>
function Counter(Remove) {
let count = ref(0);
return el(
"div",
{ react: count.Reg },
el("h1", {}, () => count.value),
el("button", { onClick: () => count.Update((v) => v + 1) }, "+"),
el("button", { onClick: () => count.Set(count.value - 1) }, "-"),
el("hr", {})
);
}
Engage(Counter, (el) => document.body.replaceChildren(el));
</script>
</html>
Add Elements Dynamically along side html elements
Caution: Removing an element with a selector can cause a memory leak, always use "component.Remove()" on the object returned by Engage()
:
let component = Engage(App, (el)=>document.body.appendChild(el));
component.Remove()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multiple Instances</title>
<script src="https://www.unpkg.com/@xipjs/[email protected]/min/xip.gt.min.js"></script>
</head>
<body>
<button onclick="NewCounter()">New</button>
</body>
<script>
function Counter(Remove) {
let count = ref(0);
return el(
"div",
{ react: count.Reg },
el("h1", {}, () => count.value),
el("button", { onClick: Remove }, "Remove"),
el("button", { onClick: () => count.Set(count.value + 1) }, "+"),
el("button", { onClick: () => count.Set(count.value - 1) }, "-"),
el("hr", {})
);
}
function NewCounter() {
// Activate a new Counter Component
let instance = Engage(
() => Counter(() => instance.Remove()),
(el) => {
document.body.appendChild(el);
}
);
}
</script>
</html>
Behaviour and Optimisation
UML
The following examples demonstrate how components are rendered and how the process can be optimized.
let component = () => {
let word = ref("Hello");
function Update() {
// Switch between "Hello" and "World"
word.Update((v) => (v === "hello" ? "World" : "Hello"));
}
let displayValue = () => word.value;
return (
<div id="a" react={word.Reg}>
<button onClick={Update}>change</button>
<div id="b">
<div id="c">{displayValue}</div>
</div>
</div>
);
};
flowchart TD
a{user} --> |clicks| button
button --> |Update| div#a
div#a --> |Render?| button
div#a -->|Render?| div#b
div#a --> |NothingToRender| n(DoNothing)
div#b --> |Render?| div#c
div#b --> |NothingToRender| n(DoNothing)
div#c --> |NothingToRender| n(DoNothing)
div#c --> |Render?| displayValue
displayValue --> |replaceWithNewValue|TextNodeInDom
Optimized:
let component = () => {
let word = ref("Hello");
function Update() {
// Switch between "Hello" and "World"
word.Update((v) => (v === "hello" ? "World" : "Hello"));
}
let displayValue = () => word.value;
return (
<div id="a">
<button onClick={Update}>change</button>
<div id="b">
<div id="c" react={word.Reg}>
{displayValue}
</div>
</div>
</div>
);
};
flowchart TD
a{user} --> |clicks| button
button --> |Update| div#c
div#c --> |NothingToRender| n(DoNothing)
div#c --> |Render?| displayValue
displayValue --> |replaceWithNewValue|TextNodeInDom
Intercept Element Removal
If the onRemove
element attribute returns a promise, the renderer will wait for the promise to resolve before removing that element.
If it rejects the element won't be removed.
Any updates that do not result in the removal of the element will still work while waiting for the promise to resolve (The application will remain responsive, including children of the intercepted element).
Syntax
JSX and Hyperscript are two syntax options that represent html inside JavaScript. Hyperscript is valid JavaScript, so JSX must be converted to hyperscript before being executed by the browser.
HTML Comparison
The following 3 examples are HTML, JSX and Hyperscript producing identical results:
HTML:
<form id="form-id">
<input type="text" />
<input type="checkbox" name="checkbox" id="1" />
<button type="submit">Submit</button>
</form>
<script>
document
.getElementById("form-id")
.addEventListener("click", (e) => console.log(e));
</script>
JSX:
let component = () => {
return (
<form onClick={(e) => console.log(e)}>
<input type="text" />
<input type="checkbox" name="checkbox" id="1" />
<button type="submit">Submit</button>
</form>
);
};
Hyperscript
let component = () => {
return el(
"form",
{ onClick: (e) => console.log(e) },
el("input", { type: "text" }),
el("input", { type: "checkbox", name: "checkbox", id: "1" }),
el("button", { type: "submit" }, "Submit")
);
};