Giter Club home page Giter Club logo

wildhoney / switzerland Goto Github PK

View Code? Open in Web Editor NEW
270.0 10.0 15.0 40.06 MB

🇨🇭Switzerland takes a functional approach to Web Components by applying middleware to your components. Supports Redux, attribute mutations, CSS variables, React-esque setState/state, etc… out-of-the-box, along with Shadow DOM for style encapsulation and Custom Elements for interoperability.

Home Page: https://switzerland.herokuapp.com/

License: MIT License

TypeScript 87.00% CSS 11.66% JavaScript 0.46% Makefile 0.74% Shell 0.15%
webcomponents virtual-dom shadow-dom functional immutable css-variables custom-elements components component component-architecture

switzerland's Introduction

Switzerland

Switzerland takes a functional approach to web components using Preact with shadow DOM for style encapsulation, custom elements for interoperability and server-side rendering for universality.

npm   License MIT   code style: prettier

yarn: yarn add switzerland
npm: npm install switzerland
cdn: https://cdn.jsdelivr.net/npm/switzerland@latest/dist/index.client.js

Screenshot


Contents

  1. Getting Started
  2. Import Maps
  3. Managing State
  4. Applying Styles
  5. Data Fetching
  6. Environment Context
  7. Extending Elements

Getting Started

Switzerland optionally begins with server-side rendering with hydration on the client thanks to declarative shadow DOM — with our components looking very familiar due to our usage of Preact.

import { create } from "switzerland";

export default create("x-countries", () => {
  return (
    <ul>
      <li>Japan</li>
      <li>Croatia</li>
      <li>Singapore</li>
    </ul>
  );
});

Once we've defined our x-countries component we are able to both render it on the server and hydrate it on the client as a standard <x-countries /> DOM element. We can then take a step further and allow our countries to be passed as a HTML attribute on the DOM node using <x-countries list="Japan,Croatia,Singapore">.

import { create, type, use } from "switzerland";

type Attrs = {
  countries: string[];
};

export default create<Attrs>("x-countries", () => {
  const attrs = use.attrs({
    countries: type.Array(type.String),
  });

  return (
    <ul>
      {attrs.countries.map((country) => (
        <li key={country}>{country}</li>
      ))}
    </ul>
  );
});

Using our component from within a Node environment requires us to use the exported asynchronous render function; we can specify an optional second parameter to the function, however our component currently doesn't perform data fetching or media inclusion and so is unnecessary.

import { render } from "switzerland";

app.get("/", async (_, response) => {
  const html = await render(<Countries list="Japan,Croatia,Singapore" />);
  response.send(html);
});

As our components are self-contained modules, any changes to their attributes will initiate a re-render of the component's tree – regardless of whether those attributes change from inside another component or through vanilla DOM accessors.

const node = document.querySelector("x-countries");
node.attributes.values = `${node.attributes.values},Ukraine,Maldives`;

Import Maps

Switzerland doesn't need to be compiled except for optional TypeScript and JSX transpiling because it uses native ES modules in the browser and Node 16+ on the server. It achieves this by using node_modules when rendering on the server using named imports, and in the browser it uses import maps to resolve those named imports to CDN URLs which offers enhanced caching. We provide a utility for the server to automatically generate the import maps for your application based on its dependencies.

import fs from "node:fs";
import { imports, render } from "switzerland";

app.get("/", async (_, response) => {
  const html = await render(<Countries list="Japan,Croatia,Singapore" />);
  const importMap = await imports({ path: path.resolve("../app/src") });

  response.send(`
        <head>
            <script type="importmap">
                ${importMap}
            </script>
        </head>

        <body>
            ${html}
        </body>
    `);
});

You need to give the imports function the base path of your Switzerland components. It will then traverse the files using ts-morph which provides an abstract syntax tree (AST) of your code and allows us to pick out the external dependencies; it then iteratively matches each of those dependencies it finds to the versions installed by your chosen package manager. We use the @jspm/generator package to resolve the dependencies to jspm.io URLs by default – however you may also pass the provider option to change the provider.

Once you have the import map configured, when rendering Switzerland components in the browser it will use those CDN URLs and prevent any need to package up dependencies via a bundler. You can focus purely on the simple task of transpiling TypeScript and JSX into native ES modules using nothing more than tsc – although if you want to minify you may need to add Terser.

{
  "include": ["src"],

  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",

    "module": "esnext",
    "moduleResolution": "nodenext",
    "esModuleInterop": true,
    "target": "esnext",
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "declaration": true
  }
}

Managing State

Since we use Preact to render Switzerland's components the API should already be familiar. For ease of use we re-export Preact's hook functions but you may also use them directly from Preact.

import { create, use } from "switzerland";

export default create("x-countries", () => {
  const [countries, setCountries] = use.state([
    "Japan",
    "Croatia",
    "Singapore",
  ]);

  return (
    <ul>
      {countries.map((country) => (
        <li key={country}>{country}</li>
      ))}
    </ul>
  );
});

Applying Styles

Styles within a shadow boundary allow for encapsulation which means we can use regular CSS documents scoped to our component's tree. We can attach our stylesheets to our component by using a regular link node, although Switzerland provides a node utility for StyleSheet and Variables — the latter applies custom variables to your component tree allowing CSS to access those JavaScript variables. We use the use.path hook to resolve media — CSS documents, images, etc... — relative to our component.

import { create, node, use } from "switzerland";

export default create("x-countries", () => {
  const path = use.path(import.meta.url);
  const [countries, setCountries] = use.state([
    "Japan",
    "Croatia",
    "Singapore",
  ]);

  return (
    <>
      <ul>
        {countries.map((country) => (
          <li key={country}>{country}</li>
        ))}
      </ul>

      <node.Variables
        backgroundColour={countries.length === 0 ? "#8ECCD4" : "#FBDEA3"}
      />

      <node.StyleSheet href={path("./styles/default.css")} />
      <node.StyleSheet
        href={path("./styles/mobile.css")}
        media="(max-width: 768px)"
      />
      <node.StyleSheet href={path("./styles/print.css")} media="print" />
    </>
  );
});

We can then be quite loose when applying those styles to our component knowing that the shadow boundary will prevent styles from leaking out — we use a CSS variable to apply a conditional background colour with a fallback.

:host {
  box-shadow: 0 0 5px #e8c5b0;
}

ul {
  background-color: var(--background-color, "#E39AC7");
}

Data Fetching

Since Switzerland allows for server-side rendering by default a use.loader utility hook is provided for fetching data – although you may choose to use any other third-party fetching utility or a simple useEffect and that is fine too. Using loader hook allows for fetching data server-side and then preventing a re-fetch on the client; we achieve this by rendering our components twice in the asynchronous render function we covered earlier and then including the serialised data in the tree.

import { create, use } from "switzerland";

export default create("x-countries", () => {
  const { data, loading, error } = use.loader(
    "x-countries",
    () =>
      fetch("https://www.example.org/countries").then((response) =>
        response.json()
      ),
    null
  );

  return loading ? (
    <p>Loading&hellip;</p>
  ) : (
    <ul>
      {data.map((country) => (
        <li key={country}>{country}</li>
      ))}
    </ul>
  );
});

We provide a unique ID to the loader function which should identify the individual request to prevent duplicates and to allow for reconciliation on the client. With the dependencies argument in third position we can re-invoke the loader client-side whenever a parameter changes; in our case we probably don't want to re-fetch given nothing changes but if fetching by a given list we might expect the current list of countries to be provided as dependencies.

Environment Context

Providing the environment context requires some user configuration on the server side — the render function takes an optional second parameter which allows us to specify both the root directory on the web-server and optionally the domain we're running the server on.

import App from "./App";
import { preload, render } from "switzerland";

const vendor = path.resolve("..");

const options = {
  path: process.env["DOMAIN"]
    ? `https://${process.env["DOMAIN"]}/client`
    : "http://localhost:3000/client",
  root: vendor,
};

app.get("/", async (_, response) => {
  const html = await render(
    <Countries list="Japan,Croatia,Singapore" />,
    options
  );
  response.send(html);
});

We use these options to resolve media using the use.path hook with import.meta.url relative to the component – on the server we need to know the root directory in order to achieve this. On the client-side however it's slightly more simple since we know the root based on each components' path. Likewise with the path option where we specify the domain the web-server is running on; we use this to provide absolute paths to media so that components can be utilised in third-party applications, however since it's optional we use the aforementioned root to specify a relative path which is perfectly fine when we're only using our components on our own web-server.

Using the use.env hook we can access these defined parameters as well as a few additional items.

import { create, use } from "switzerland";

export default create("x-countries", () => {
  const { path, root, node, isServer, isClient } = use.env();

  return (
    <>
      {node && <h1>Hey {node.nodeName}!</h1>}

      <p>Server: {isServer}</p>
      <p>Client: {isClient}</p>

      <ul>
        <li>Japan</li>
        <li>Croatia</li>
        <li>Singapore</li>
      </ul>
    </>
  );
});

Extending Elements

You may also extend native HTML elements using the x-hello:button syntax in the create function – it'll create a x-hello custom element that extends from the button constructor allowing you to add your own twist to it.

import { create, use } from "switzerland";

export default create("x-hello:button", () => {
  const handleClick = use.callback((): void => console.log("Hello!"), []);

  return <button onClick={handleClick}>Say Hello!</button>;
});

switzerland's People

Contributors

bogas04 avatar wildhoney avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

switzerland's Issues

reactions to a great project start

@Wildhoney Great work! The project seems extremely promising. Some suggestions for improving:

Get people excited Switzerland

  • Add statistics about artifact sizes (gzipped & not)?
  • Discuss strategies (or provide links to strategies that will work with Switzerland) for:
    • mobile
    • progressive
    • offline strategies (I see the the example support offline via ServiceWorker which is great)
    • universal rendering

Example

  • Consider following the guidelines from todomvc.com and submitting the example TODO app to them.
  • The build.js in the example is almost 1.5 MB. Need to add justification for why or split it into vendor and app bundles.
  • Example https://switzerland.herokuapp.com/ shows WSOD (white-screen-off-death) from Chrome on my Motorola Moto G running Marshmallow (6.0.2)

Development

  • Add a section for contributing to the project. Explain that you're using the AVA test runner and the XO linter.

Typescript support

Is there any plans for adding typescript typings? It is very promising project and I think it would be great if it is able to be used in typescript-written projects.

Rethink alias function

Needs to function across multiple instances of the Switzerland library. With the current implementation it wouldn’t because the set is unique to a single instance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.