Skip to content

Commit

Permalink
Add new router feature
Browse files Browse the repository at this point in the history
  • Loading branch information
C-D-Lewis committed Mar 15, 2024
1 parent ae4eb7d commit e839ddb
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 14 deletions.
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,25 +123,26 @@ The API is split into two sections - component construction and app helpers.

* [Create components](#component)
* [`.asFlex()`](#asflex)
* [`.setStyles()` / `setAttributes()`](#setstyles--setattributes)
* [`.setChildren()` / `addChildren`](#setchildren--addchildren)
* [`.onClick()` / `onHover()` / `onChange()`](#onclick--onhover--onchange)
* [`.setText()` / `setHtml()`](#settext--sethtml)
* [`.setStyles()` / `.setAttributes()`](#setstyles--setattributes)
* [`.setChildren()` / `.addChildren`](#setchildren--addchildren)
* [`.onClick()` / `.onHover()` / `.onChange()`](#onclick--onhover--onchange)
* [`.setText()` / `.setHtml()`](#settext--sethtml)
* [`.onDestroy()`](#ondestroy)
* [`.onEvent()`](#onevent)
* [`.displayWhen()`](#displaywhen)
* [`.empty()`](#empty)

### App helpers

* [`fabricate` / `fab` helpers](#fabricate--fab-helpers)
* [`fabricate` helpers](#fabricate-helpers)
* [`.isNarrow()`](#isnarrow)
* [`.app()`](#app)
* [`.declare()`](#declare)
* [`.onKeyDown()`](#onkeydown)
* [`.update()` / `.onUpdate()`](#update--onupdate)
* [`.buildKey()`](#buildkey)
* [`.conditional()`](#conditional)
* [`.router()` / `.navigate()`](#router--navigate)


### Create components
Expand Down Expand Up @@ -538,6 +539,34 @@ fabricate('Column')
]);
```

### `.router()` / `.navigate()`

For a multi-page app, use `.router()` to declare pages to be displayed when
`.navigate()` is used, usually at the top level.

```js
const HomePage = () => fabricate('Column')
.setChildren([
fabricate('h1').setText('This is HomePage'),
fabricate('Button', { text: 'Go to StatusPage' })
.onClick(() => fabricate.navigate('/status')),
]);
const StatusPage = () => fabricate('Column')
.setChildren([
fabricate('h1').setText('This is StatusPage'),
fabricate('Button', { text: 'Go to HomePage' })
.onClick(() => fabricate.navigate('/')),
]);

const App = () => fabricate.router({
'/': HomePage,
'/status': StatusPage,
});

// Use as the root app element
fabricate.app(App);
```


## Built-in components

Expand Down
36 changes: 36 additions & 0 deletions examples/router.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>fabricate.js example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
* { font-family: sans-serif; }
</style>
</head>
<body>
<script type="text/javascript" src="../../fabricate.js"></script>

<script>
const HomePage = () => fabricate('Column')
.setChildren([
fabricate('h1').setText('This is HomePage'),
fabricate('Button', { text: 'Go to StatusPage' })
.onClick(() => fabricate.navigate('/status')),
]);
const StatusPage = () => fabricate('Column')
.setChildren([
fabricate('h1').setText('This is StatusPage'),
fabricate('Button', { text: 'Go to HomePage' })
.onClick(() => fabricate.navigate('/')),
]);

const App = () => fabricate.router({
'/': HomePage,
'/status': StatusPage,
});

// Use as the root app element
fabricate.app(App);
</script>
</body>
</html>
111 changes: 106 additions & 5 deletions fabricate.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const _fabricate = {
},
/** Minimum children before groups are added with timeout */
MANY_CHILDREN_GROUP_SIZE: 50,
StateKeys: {
Init: 'fabricate:init',
Created: 'fabricate:created',
Route: 'fabricate:route',
},

// Main library state
state: {},
Expand All @@ -23,6 +28,8 @@ const _fabricate = {
options: undefined,
onDestroyObserver: undefined,
ignoreStrict: false,
router: {},
route: '',

// Internal helpers
/**
Expand Down Expand Up @@ -440,16 +447,16 @@ const fabricate = (name, customProps) => {
_fabricate.stateWatchers.push({ el, cb, watchKeys });
el.onDestroy(_unregisterStateWatcher);

if (watchKeys.includes('fabricate:created')) {
if (watchKeys.includes(_fabricate.StateKeys.Created)) {
// Emulate onCreate immediately if this method is used
cb(el, _fabricate.getStateCopy(), ['fabricate:created']);
cb(el, _fabricate.getStateCopy(), [_fabricate.StateKeys.Created]);
}

return el;
};

/**
* Optional on create handler, alternative to 'fabricate:created' event.
* Optional on create handler, alternative to _fabricate.StateKeys.Created event.
*
* @param {Function} cb - Callback to be notified.
* @returns {FabricateComponent} Fabricate component.
Expand Down Expand Up @@ -557,6 +564,7 @@ fabricate.update = (param1, param2) => {
return;
}

// Should never happen
throw new Error(`Invalid state update: ${typeof param1} ${typeof param2}`);
};

Expand Down Expand Up @@ -618,7 +626,7 @@ fabricate.app = (rootCb, initialState = {}, opts = {}) => {
_fabricate.onDestroyObserver.observe(root, { subtree: true, childList: true });
}

_notifyStateChange(['fabricate:init']);
_notifyStateChange([_fabricate.StateKeys.Init]);
};

/**
Expand Down Expand Up @@ -687,18 +695,87 @@ fabricate.conditional = (testCb, builderCb) => {
return wrapper;
};

/**
* Use a router to show many pages inside the parent component.
*
* Example:
* {
* '/': HomePage,
* '/user': UserPage,
* }
*
* @param {object} router - Object of routes and components to render.
* @returns {void}
*/
fabricate.router = ((router) => {
// Validate
if (!Object.entries(router).every(([route, builderCb]) => {
const isValid = route.startsWith('/') && typeof builderCb === 'function';
return isValid;
})) {
throw new Error('Every route in router must be builder function');
}
if (!router['/']) {
throw new Error('Must provide initial route /');
}

_fabricate.router = router;

// Add all routes in router
const wrapper = fabricate('div');
Object.entries(router).forEach(([route, builderCb]) => {
wrapper.addChildren([
fabricate.conditional(
(state) => state[_fabricate.StateKeys.Route] === route,
builderCb,
),
]);
});

// Initial route is '/'
fabricate.update(_fabricate.StateKeys.Route, '/');

return wrapper;
});

/**
* Navigate to a given route. If it exists, it is rendered.
*
* @param {string} route - Route to show.
*/
fabricate.navigate = ((route) => {
if (!_fabricate.router[route]) {
throw new Error(`Unknown route: ${route}`);
}

_fabricate.route = route;
fabricate.update(_fabricate.StateKeys.Route, route);
});

/// //////////////////////////////////// Built-in Components ///////////////////////////////////////

/**
* Row built-in component.
*/
fabricate.declare('Row', () => fabricate('div').asFlex('row'));

/**
* Column built-in component.
*/
fabricate.declare('Column', () => fabricate('div').asFlex('column'));

/**
* Text built-in component.
*/
fabricate.declare('Text', ({ text } = {}) => {
if (text) throw new Error('Text component text param was removed - use setText instead');

return fabricate('p').setStyles({ fontSize: '1rem', margin: '5px' });
});

/**
* Image built-in component.
*/
fabricate.declare('Image', ({ src = '', width, height } = {}) => {
if (width || height) throw new Error('Image component width/height params removed - use setStyles instead');

Expand All @@ -707,6 +784,9 @@ fabricate.declare('Image', ({ src = '', width, height } = {}) => {
.setAttributes({ src });
});

/**
* Button built-in component.
*/
fabricate.declare('Button', ({
text = 'Button',
color = 'white',
Expand Down Expand Up @@ -736,6 +816,9 @@ fabricate.declare('Button', ({
})
.setText(text));

/**
* NavBar built-in component.
*/
fabricate.declare('NavBar', ({
title = 'NavBar Title',
color = 'white',
Expand Down Expand Up @@ -772,6 +855,9 @@ fabricate.declare('NavBar', ({
return navbar;
});

/**
* TextInput built-in component.
*/
fabricate.declare('TextInput', ({
placeholder = 'Enter value',
color = 'black',
Expand All @@ -790,6 +876,9 @@ fabricate.declare('TextInput', ({
})
.setAttributes({ type: 'text', placeholder }));

/**
* Loader built-in component.
*/
fabricate.declare('Loader', ({
size = 48,
lineWidth = 5,
Expand Down Expand Up @@ -824,6 +913,9 @@ fabricate.declare('Loader', ({
return container;
});

/**
* Card built-in component.
*/
fabricate.declare('Card', () => fabricate('Column')
.setStyles({
width: 'max-content',
Expand All @@ -833,15 +925,21 @@ fabricate.declare('Card', () => fabricate('Column')
overflow: 'hidden',
}));

/**
* Fader built-in component.
*/
fabricate.declare('Fader', ({
durationS = '0.6',
delayMs = 300,
} = {}) => fabricate('div')
.setStyles({ opacity: 0, transition: `opacity ${durationS}s` })
.onUpdate((el) => {
setTimeout(() => el.setStyles({ opacity: 1 }), delayMs);
}, ['fabricate:created']));
}, [_fabricate.StateKeys.Created]));

/**
* Pill built-in component.
*/
fabricate.declare('Pill', ({
text = 'Pill',
color = 'white',
Expand All @@ -866,6 +964,9 @@ fabricate.declare('Pill', ({
})
.setText(text));

/**
* FabricateAttribution built-in component.
*/
fabricate.declare('FabricateAttribution', () => fabricate('img')
.setAttributes({
src: 'https://raw.githubusercontent.com/C-D-Lewis/fabricate.js/main/assets/logo_small.png',
Expand Down
17 changes: 13 additions & 4 deletions test/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ type AppState = {
};

/**
* App component.
*
* @returns {FabricateComponent} The component.
* TestPage
* @returns {FabricateComponent} TestPage component.
*/
const App = (): FabricateComponent<AppState> => fabricate('Column')
const TestPage = () => fabricate('Column')
.setChildren([
fabricate('h3').setText('Test TypeScript app'),
fabricate('p')
Expand All @@ -25,6 +24,16 @@ const App = (): FabricateComponent<AppState> => fabricate('Column')
}, ['counter']),
]);

/**
* App component.
*
* @returns {FabricateComponent} The component.
*/
const App = (): FabricateComponent<AppState> => fabricate.router({
'/': TestPage,
});
setTimeout(() => fabricate.navigate('/'), 1000);

const initialState = { counter: 0, updated: false };
const options: FabricateOptions = {
logStateUpdates: true,
Expand Down
Loading

0 comments on commit e839ddb

Please sign in to comment.