Places.js is a HTML-first framework inspired by htmx and Solid.js signals with these features.
- Component agnostic stores that can be integrated with vanilla JavaScript or other frameworks.
- HTML-first pages configured through static site generation or server-side rendering.
- No build step. Places.js can be downloaded from a CDN.
- Partial re-rendering with signals.
- Zero dependencies. Places.js is written entirely in vanilla JavaScript.
To ensure quality, all code is written by hand and reviewed by a human without AI assistance.
Places.js is optimized for fast rendering. Here are benchmarks for Places.js v0.1.0 using the Krauset JavaScript Framework Benchmark compared to other frameworks. All bechmarks are measured in miliseconds.
| Places.js | Solid.js | Svelte | Lit | Vue | React | |
|---|---|---|---|---|---|---|
| Rendering 1,000 rows | 19.8 | 21.4 | 21.8 | 23.3 | 23.9 | 24.7 |
| Selecting 1 row | 2.6 | 3.2 | 5.0 | 5.4 | 3.7 | 5.5 |
| Clear 1,000 rows | 9.7 | 10.4 | 10.4 | 11.8 | 12.0 | 16.9 |
Getting started
Download the places-js-latest.js file from the jsDelivr
CDN
or run npx jsr add @createthirdplaces/places-js.
See the reference guide for a detailed tutorial.
Creating a Places.js component to list data
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Game stores</title>
<link rel="stylesheet" href="/styles/ui/list/gameStoreStyles.css" />
<link rel="preconnect" href="localhost:7070" />
<link rel="preconnect" href="api.gatherspiel.com" />
<script type="module">
import { PresentationComponent } from "/lib/places-js-latest.js";
customElements.define("presentation-component", PresentationComponent);
</script>
<link rel="icon" type="image/ico" href="/assets/images/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<presentation-component
data-api-path="/searchLocations?area=dmv&locationType=gameStores"
data-api-root="{apiRoot}"
data-template
>
<ul>
<li class="game-store-list-item">
<a class="btn secondary" href="{url}"> {id} </a>
<p>Location: {location}</p>
</li>
</ul>
</presentation-component>
</body>
</html>
Here is an example of a HTML-first page running live in production with no JavaScript.
Contributing
Share feedback on Places.js by creating an issue or emailing gulu@createthirdplaces.org. One can also share feedback by commenting on an existing issue.
Code and website contributions
Places.js is looking for help in the following areas. See the the issues page for more information
- Adding more unit test coverage.
- Compatibility with other hypermedia UI tools including Datastar and htmx
- Implementing hypermedia controls.
- Testing integration with Leaflet and other mapping libraries.
Also, all Places.js contributions including code changes, comments, or new issues must be made by humans This means no use of ChatGPT, Claude, or any other AI tool.
Additional Examples
Creating a component with a template to list events and custom reactivity configuration
Component
<presentation-component data-store = "search-results-store">
<div>
<p id="no-events-found">No events found</p>
<h1 id="search-results-header">Event search results</h1>
<ul data-template=data>
<li>
<a class="btn secondary" href="{url}">
{eventName}
</a>
<div class="event-time">
{eventTime}
</div>
<div class="event-location">
{location}
</div>
</li>
</ul>
</div>
</presentation-component>
Store configuration
function getSearchResultsQueryConfig(searchParams) {
let url;
// Logic to determine API url for store goes here.
return {
url: url,
};
}
const presentationSignals = {
groupData: {
update: {
recurringEventDays: (groupData) => {
if (groupData.recurringEventDays.length > 0) {
return `<span class="group-search-details"><b>Days:</b> ${groupData.recurringEventDays.join(", ")}</span>`;
}
return ``;
},
gameTypeTags: (groupData) => {
if (groupData.gameTypeTags.length > 0) {
return `<span class="group-search-details"><b>Game types:</b> ${groupData.gameTypeTags.join(", ")}</span>`;
}
return ``;
},
},
},
eventData: {
id: (eventData) => eventData.eventId,
update: {
eventTime: (eventData) => {
if (eventData.isRecurring) {
return `${eventData.dayOfWeek}s at ${eventData.nextEventTime}`;
}
return `${eventData.nextEventDate} at ${eventData.nextEventTime}`;
},
location: (eventData) => {
return `${convertLocationDataForDisplay(eventData.eventLocation)}`;
},
url: (eventData) => {
return `/html/groups/event.html?id=${eventData.eventId}&groupId=${eventData.groupId}`;
},
},
},
};
export const SEARCH_RESULTS_STORE = DataStore.createWithApiLoadSignal({
queryConfig: getSearchResultsQueryConfig,
presentationSignals: presentationSignals,
storeName: "search-results-store",
});
Dynamically adding Places.js components with JavaScript.
<html>
<head>
<script type="module">
import {TestRunner} from '/lib/TestRunner.js';
import {ListUpdaterTests} from '/src/listUpdaterTests.js';
import {PerformanceTests} from '/src/performanceTests.js';
import {PresentationComponent} from '/lib/places-js-min.js';
customElements.define('presentation-component',PresentationComponent);
const tests = [
new ListUpdaterTests(),
new PerformanceTests()
];
window.addEventListener("load",()=>{
const testDOM = [];
for(let i=0; i<tests.length;i++){
const test = tests[i];
TestRunner.createRunnerWithTests(test);
testDOM.push(`
<details open>
<summary>${test.constructor.name}</summary>
<presentation-component data-store="${test.getName()}">
<ul data-template="testResults">
<li>
<h2>{name}</h2>
<p>{message}</p>
<p>{isPass}</p>
</li>
</ul>
</presentation-component>
</details>
`);
}
document.getElementById("test-results")
.innerHTML = testDOM.join("\n");
document.getElementById("run-all")
.addEventListener("click",()=>{
TestRunner.runAllTests();
});
});
</script>
</head>
<body>
<h1>Tests</h1>
<button id="run-all">Run all tests</button>
<div id="test-results"></div>
</body>
</html>
Creating a minified build
Run the command npx terser places-js-latest.js -o places-js-min.js --compress --mangle
Extending Places.js
- Places.js prioritizes a minialist feature set. Developers are encouraged to create custom implementations of the Places.js component and data store classes.