https://github.com/adebola-io/unfinished
JSX -> DOM nodes.
https://github.com/adebola-io/unfinished
Last synced: 7 months ago
JSON representation
JSX -> DOM nodes.
- Host: GitHub
- URL: https://github.com/adebola-io/unfinished
- Owner: adebola-io
- License: mit
- Created: 2024-10-12T12:46:06.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2024-11-24T07:26:14.000Z (7 months ago)
- Last Synced: 2024-11-24T08:23:36.774Z (7 months ago)
- Language: JavaScript
- Homepage:
- Size: 286 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# unfinished
[](https://www.npmjs.com/package/@adbl/unfinished)
`unfinished` is a(nother) library for building web apps with JSX. It has a router. The name is tongue-in-cheek.
## Table of Contents
- [unfinished](#unfinished)
- [Table of Contents](#table-of-contents)
- [Key Features](#key-features)
- [Installation](#installation)
- [Usage](#usage)
- [Quick Start](#quick-start)
- [Understanding Cells.](#understanding-cells)
- [Rendering Lists](#rendering-lists)
- [Conditional Rendering](#conditional-rendering)
- [Routing](#routing)
- [Setting Up the Router](#setting-up-the-router)
- [Implementing the Router](#implementing-the-router)
- [Nested Routing](#nested-routing)
- [Lazy Loading Routes](#lazy-loading-routes)
- [Programmatic Navigation](#programmatic-navigation)
- [Dynamic Route Parameters](#dynamic-route-parameters)
- [Wildcard Routes](#wildcard-routes)
- [Stack Mode Navigation](#stack-mode-navigation)
- [Enabling Stack Mode](#enabling-stack-mode)
- [Example Stack Mode Flow](#example-stack-mode-flow)
- [Keep Alive Routes](#keep-alive-routes)
- [Outlet Animations](#outlet-animations)
- [Basic Animation Setup](#basic-animation-setup)
- [Advanced Animation Control](#advanced-animation-control)
- [Router Relays](#router-relays)
- [Basic Usage](#basic-usage)
- [Lifecycle Behavior](#lifecycle-behavior)
- [Why This Library?](#why-this-library)
- [License](#license)## Key Features
- **Lightweight**: Minimal overhead for optimal performance
- **JSX Support**: Familiar syntax for React developers
- **Reactive**: Built-in reactivity with the [`@adbl/cells`](https://github.com/adebola-io/cells) library.
- **Routing**: Built-in routing system for single-page applications
- **Hot Module Replacement**: Supports hot module replacement for a seamless development experience## Installation
To create a new project with this library, run the following command:
```bash
npx @adbl/unfinished-start
```Follow the prompts to configure your project, then:
```bash
cd your-project-name
npm install
npm run dev
```Open `http://localhost:5229` in your browser to see your new app!
## Usage
### Quick Start
Here's a simple example to get you started with the library:
```jsx
import { Cell } from '@adbl/cells';const Counter = () => {
const count = Cell.source(0);const increaseCount = () => {
count.value++;
};return (
{count}
Increment
);
};document.body.append();
```The example above will make a simple counter component that will increment the count when the button is clicked.
### Understanding Cells.
Cells are a reactive data structure that can be used to store and update data in a reactive way. They are similar to Reactive Variables in other reactive programming libraries.
The `Cell.source` function is used to create a new cell with an initial value. The `value` property of the cell can be accessed to get or set the current value of the cell.
```jsx
import { Cell } from '@adbl/cells';const count = Cell.source(0);
count.value++; // Increments the value of the cell by 1
```In the example above, the `count` cell is initialized with a value of 0. The `value` property of the cell is then incremented by 1.
Crucially, within JSX templates, you use the cell object directly (without .value) to maintain reactivity:
```jsx
Count: {count}
```Whenever `count.value` is modified, the UI will automatically update to reflect the new value. This reactive behavior is the foundation of how `unfinished` handles dynamic updates.
### Rendering Lists
The `For` function can be used to efficiently render lists:
```jsx
import { For } from '@adbl/unfinished';
import { Cell } from '@adbl/cells';const listItems = Cell.source([
'Learn the library',
'Build a web app',
'Deploy to production',
]);const TodoList = () => {
return (
-
{item} (Index: {index})
{For(listItems, (item, index) => (
))}
);
};
document.body.append();
// Later, when the listItems cell updates, the DOM will be updated automatically
listItems.value.push('Celebrate success');
```
> The `For` function is aggressive when it comes to caching nodes for performance optimization.
> This means that the callback function provided to `For` **should** be pure and not rely on external state or produce side effects, because the callback function might not be called when you expect it to be.
>
> Here's an example to illustrate why this is important:
>
> ```tsx
> import { For } from '@adbl/unfinished';
> import { Cell } from '@adbl/cells';
>
> let renderCount = 0;
> const items = Cell.source([
> { id: 1, name: 'Alice' },
> { id: 2, name: 'Bob' },
> { id: 3, name: 'Charlie' },
> ]);
>
> const List = () => {
> return (
>
-
> {item.name} (Renders: {renderCount})
>
> {For(items, (item) => {
> renderCount++; // This is problematic!
> return (
>
> );
> })}
>
> );
> };
>
> document.body.append();
> // Initial output:
> // - Alice (Renders: 1)
> // - Bob (Renders: 2)
> // - Charlie (Renders: 3)
>
> // Later:
> items.value.splice(1, 0, { id: 4, name: 'David' });
> // Actual output:
> // - Alice (Renders: 1)
> // - David (Renders: 4)
> // - Bob (Renders: 2)
> // - Charlie (Renders: 3)
> ```
>
> In the example, when we splice a new item into the middle of the array, the `For` function reuses the existing nodes for Alice, Bob, and Charlie. It only calls the callback function for the new item, David. This leads to an unexpected render count for David.
>
> To avoid this issue, use the reactive index provided by `For`:
>
> ```tsx
> const List = () => {
> return (
>
-
> {item.name} (Index: {index})
>
> {For(items, (item, index) => {
> return (
>
> );
> })}
>
> );
> };
> ```
>
> This approach ensures correct behavior regardless of how the array is modified, as the index is always up-to-date.
### Conditional Rendering
Use the `If` function for conditional rendering:
```jsx
import { If } from '@adbl/unfinished';
import { Cell } from '@adbl/cells';
const isLoggedIn = Cell.source(false);
const username = Cell.source('');
// Greeting component for logged in users
function Greeting() {
return (
Welcome back, {username}!
{
isLoggedIn.value = false;
}}
>
Logout
);
}
// Login page component for non-logged in users
function LoginPage() {
return (
Please log in
{
username.value = input.value;
}}
/>
{
isLoggedIn.value = true;
}}
>
Login
);
}
function LoginStatus() {
return
{If(
// Condition: check if the user is logged in
isLoggedIn,
// If true, render the Greeting component
Greeting,
// If false, render the LoginPage component
LoginPage
)}
);
// Appending the LoginStatus component to the body
document.body.append();
```
## Routing
The library includes a routing system for single-page applications.
### Setting Up the Router
```jsx
import { createWebRouter, type RouteRecords } from '@adbl/unfinished/router';
const Home = () => {
return
Welcome to the Home Page
;};
const About = () => {
return
About Us
;};
const NotFound = () => {
return
404 - Page Not Found
;};
const routes: RouteRecords = [
{ name: 'home', path: '/', component: Home },
{ name: 'about', path: '/about', component: About },
{ name: 'not-found', path: '*', component: NotFound },
];
const router = createWebRouter({ routes });
document.body.appendChild();
```
### Implementing the Router
Use the `useRouter` hook to access routing functionality from inside a component. This will prevents circular dependencies and import issues.
```jsx
import { useRouter } from '@adbl/unfinished/router';|
const App = () => {
const router = useRouter();
const { Link, Outlet } = router;
return (
Home
About
);
};
export default App;
```
### Nested Routing
The library supports nested routing for more complex application structures:
```jsx
const routes: RouteRecords = [
{
name: 'dashboard',
path: '/dashboard',
component: Dashboard,
children: [
{ name: 'overview', path: 'overview', component: Overview },
{ name: 'stats', path: 'stats', component: Stats },
],
},
];
```
```jsx
import { useRouter } from '@adbl/unfinished/router';
const Dashboard = () => {
const { Link, Outlet } = useRouter();
return (
Dashboard
Overview
Stats
);
};
```
### Lazy Loading Routes
Implement code splitting with lazy-loaded routes:
```javascript
const Settings = lazy(() => import('./Settings'));
```
### Programmatic Navigation
Navigate programmatically using the `navigate` method:
```jsx
const ProfileButton = () => {
const { navigate } = useRouter();
const goToProfile = () => {
navigate('/profile/123');
};
return View Profile;
};
```
### Dynamic Route Parameters
Define and access dynamic route parameters:
```javascript
{
name: 'profile',
path: 'profile/:id',
component: lazy(() => import('./Profile')),
}
const Profile = () => {
const router = useRouter();
const id = router.params.get('id');
return
Profile ID: {id}
;};
```
### Wildcard Routes
Handle 404 pages and other catch-all scenarios:
```javascript
{
name: 'not-found',
path: '*',
component: lazy(() => import('./NotFound')),
}
```
### Stack Mode Navigation
**Stack Mode** turns the router into a stack-based navigation system. This lets routes act like a stack, where each route is a unique entry that can be navigated to and from.
#### Enabling Stack Mode
To enable Stack Mode, set `stackMode: true` in your router configuration:
```tsx
const router = createWebRouter({
routes: [...],
stackMode: true
});
```
#### Example Stack Mode Flow
```tsx
// Starting at /home
router.navigate('/photos'); // Adds /photos to the stack
router.navigate('/photos/1'); // Adds /photos/1 to the stack
// Stack is now: ['/home', '/photos', '/photos/1']
router.back(); // Pops back to /photos
// Stack is now: ['/home', '/photos']
router.navigate('/settings'); // Adds /settings to the stack
// Stack is now: ['/home', '/photos', '/settings']
router.navigate('/home'); // Pops back to /home
// Stack is now: ['/home']
```
### Keep Alive Routes
Keep Alive preserves the DOM nodes of route components when navigating away, maintaining them for when users return. This is particularly useful for preserving form inputs, scroll positions, or complex component states across navigation.
```tsx
// Basic keep alive outlet
// With custom cache size, defaults to 10
```
When enabled, the router will:
- Cache the DOM nodes of routes when navigating away
- Restore the exact state when returning to the route
- Preserve scroll positions for both the outlet and window
- Maintain form inputs and other interactive elements
This is especially valuable for scenarios like:
- Multi-step forms where users navigate between steps
- Long scrollable lists that users frequently return to
- Complex interactive components that are expensive to reinitialize
- Search results pages that users navigate back and forth from
> **NOTE**: While useful, keep alive does consume more memory as it maintains DOM nodes in memory. Consider the `maxKeepAliveCount` parameter to limit cache size based on your application's needs.
### Outlet Animations
Router outlets can animate route transitions using CSS animations. This provides smooth visual feedback during navigation and enhances the overall user experience.
#### Basic Animation Setup
```tsx
```
The router will look for two CSS animations: `fade-enter` and `fade-exit` in this case. Here's a complete example:
```css
@keyframes fade-enter {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-exit {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
```
#### Advanced Animation Control
The router provides lifecycle hooks for fine-grained animation control:
```tsx
{
// Setup initial states
return {
// Optionally override animation options
name: 'slide',
duration: 500,
};
},
// Called after enter animation completes
onAfterEnter: async (nodes) => {
// Cleanup or trigger additional effects
},
// Called before exit animation starts
onBeforeExit: async (nodes) => {
// Prepare nodes for exit
},
// Called after exit animation completes
onAfterExit: async (nodes) => {
// Cleanup or trigger additional effects
},
}}
/>
```
The animation system supports:
- Parallel enter/exit animations
- Dynamic animation parameters
- Custom easing functions
- Complex multi-stage animations through hooks
- Full control over timing and synchronization
### Router Relays
Router Relays maintain continuity of DOM elements between routes. This is useful when certain elements should persist state across route changes, ensuring the same DOM node is used rather than recreating it.
#### Basic Usage
Relays allow components to be carried over between routes without unmounting or remounting. This is particularly useful for shared elements like images, avatars, or other reusable components.
```tsx
// Define a component that will persist between routes
function Photo({ src, alt }) {
return ;
}
// Define a relay wrapper for the component
function PhotoRelay({ src, alt }) {
const { Relay } = useRouter();
return ;
}
// Create relay instances in different routes
function HomeRoute() {
return (
Home
);
}
function DetailRoute() {
return (
Detail
);
}
```
In the example above, the relay ensures that the `Photo` component with the same `id` (`photo-relay`) is the same across both routes, even as the routes change.
#### Lifecycle Behavior
Relays work by matching `id` attributes between instances in the current and next route. When the route changes:
- If a relay with the same `id` exists in both the current and next route, its DOM node and state are preserved.
- If no matching relay is found in the next route, the current relay is unmounted.
- New relays in the next route are created and mounted as usual.
> **NOTE**: Relays do not handle animations or transitions. Developers can implement view transitions on their own if needed, using techniques like the native `ViewTransition` API or CSS animations in combination with relays.
## Why This Library?
This library provides a lightweight alternative to larger frameworks, offering a familiar React-like syntax with built-in routing capabilities. It's perfect for developers who want the flexibility of JSX and powerful routing without the overhead of a full framework.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.