places-js

· Codeberg.org

4 min read Original article ↗

Places.js is a HTML-first framework inspired by htmx and Solid.js signals with these features.

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.