npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ognaf/core

v1.0.0

Published

Oh God Not Another Framework - If you feel this way - this tiny library might be for you.

Downloads

106

Readme

Deprecated

I've chosen to rewrite this repo as a new repo @flemminghansen/wc-store. This doesn't mean that this repo will stop working. In fact, it should be able to survive for years to come, but I won't be adding any features to it.

The reason for this change, is that I've stopped using ShadowDom in my projects because of accessibility issues. Furthermore, I've rewritten the store to use an AbortController instead of the unsubscribe token. This means that I can share the abort signal across multiple stores, which means that I can create a StoreElement that handles this automatically. All in all, it's just a better but different solution @flemminghansen/wc-store

Lastly the name, OGNAF - I really butched that one didn't I? :D

Oh God Not Another Framework (OGNAF)

OGNAF is a tiny clientside library that aims to make it easier to used Web Components. Unlike nasty frameworks, it builds on the KISS principle (Keep It Simple, Stupid).

Why use OGNAF?

OGNAF uses the bare-bone power of the modern browser - it just makes using it a little easier. This has several benefits:

  • Extremely small - Less than 1kb minified and gzipped
  • Agnostic - Can be used with any clientside framework.
  • No dependencies - Which means fewer updates and less maintenence.
  • Modular - Each DOM element can be isolated by using shadow DOM and by applying styling inside the component. This also means that less tooling is needed for whatever bundler you are using.
  • Extendable - You can extend the classes and add your own methods and data to fit your needs.
  • TypeScript friendly.

Installation

Installation is fairly simple using Node's package manager.

In your console, run:

npm install @ognaf/core

What does OGNAF/core contain?

  • define - A simple function that simplifies the creation of web components.
  • ShadowElement - Which is basically a standard HTMLElement with an applied shadowDOM.
  • Store - A store class to create observable stores. A web component can then easily subscribe to the stores, and apply updates as the data changes.

Define

The define method is used to create define a custom element.

import { define } from "@ognaf/core";

define("hello-world", class extends HTMLElement {
    constructor() {
        super();
        this.innerHTML = "<div>Hello world!</div>";
    }
})

The define helper only define the custom element if it does not already exist. Otherwise, it will be ignored. Once the 'hello-world' component is defined, we can access it anywhere in the DOM by writing.

<hello-world></hello-world>

Options (not recommended)

You can apply variants to components by using options.

However, using the extends option is NOT recommended because the 'is' property is not supported by Safari

define("custom-p", class extends HTMLParagraphElement {
    constructor() {
      super();
      this.onclick = () => console.log("Hello world!");
      this.style.fontSize = "1.2rem";  // Set default font size
      this.style.color = "red";        // Set default text color
      this.style.padding = "10px 20px"; // Add padding
    }
  }, { extends: 'p' })

You can then apply 'custom-p' to existing paragraph tags.

<p is="custom-p">Just look how custom I am!</p>

ShadowElement

Shadow element is a small extention of the native HTMLElement that uses ShadowDOM. It defines a public property called shadow, which is of type ShadowRoot.

Unlike shadowRoot, shadow is cannot be null, which means that you don't have to check if it exists, before applying changes.

Example 1

Here is one way to create a component using shadowRoot. This is a button that shows how many times you've clicked it.

import { define, ShadowElement } from "@ognaf/core"

define('local-counter', class extends ShadowElement {
    // Here we create the button instead of writing it through
    // this.shadow.innerHTML. This way we can access countButton,
    // without having to find it in shadowDOM first.
    countButton = document.createElement('button');

    count = 0;

    getCountText() {
        return 'Count: ' + this.count;
    }

    constructor() {
        super();

        // When using shadowDOM we can use styling directly 
        // without worrying about overspill to other components.
        this.shadow.innerHTML = `
            <style>
                button {
                    background: hotpink;
                }
            </style>
        `

        // Here we append countButton directly to the shadowRoot
        this.shadow.appendChild(this.countButton)
        this.countButton.innerText = this.getCountText();
        this.countButton.onclick = () => {
            this.count += 1;
            this.countButton.innerText = this.getCountText();
        };
    }
})

Example 2

Here, instead of defining elements in the constructor, we write the entire template as a string literal. Again, this is a button that shows how many times you've clicked it.

import { define, ShadowElement } from "@ognaf/core"

define('local-counter2', class extends ShadowElement {
    count = 0;

    getCountText() {
        return 'Count: ' + this.count;
    }

    // Invoked when the ShadowElement is first connected to the DOM.
    connectedCallback() {
        // In this example we also add a button to the innerHTML
        this.shadow.innerHTML = `
            <style>
                button {
                    background: hotpink;
                }
            </style>
            <button>${this.getCountText()}</button>
        `

        // By using shadowDOM, we can isolate the querySelector to the ShadowDOM.
        // Thereby limiting the scope.
        const countButton = this.shadow.querySelector<HTMLButtonElement>('button');
        countButton.onclick = () => {
            this.count += 1;
            countButton.innerText = this.getCountText();
        }
    }
})

Store

Store is a state-holding observable. Custom components can subscribe to the Store with an updateMethod which will be called on state changes. This allows us to handle data across multiple components.

Constructor

The constructor takes the initial state of the Store as a parameter. The initial state must be an object. It then sets the Store's state and previous state to equal the initial state. Since there are no subscribers during the construction of the Store, no re-renders will be triggered.

Methods

For the types, we define the state of the store as T.

Store.getState(): T

Returns a copy of the state.

Store.getPreviousState(): T

Returns a copy of the previous state.

Store.setState(partialState: Partial<T>): void

Sets state and notify all subscribers.

Store.subscribe(updateMethod: (newState: T, oldState: T) => void): Symbol

Allows the componenent to subscribe to state changes with an updateMethod. It returns an unsubscription symbol which must be used to unsubscribe

Store.unsubscribe(subscriberToken: Symbol): void

Removes the connected updatemethod from the store.

Bad example

This is an example of what not to do. It is bad practice to edit the store's state directly from the component. Here is an example of the previous counter button where I call the store from the location.

The problem with this approach, is that we limit ourselves to the component. Also, the component becomes messy and harder to read, which is very appearant in the getCountText method.

import { define, ShadowElement, Store } from "@ognaf/core";

const store = new Store<Record<string, number>>({});

define('my-bad-counter', class extends ShadowElement {
    // Symbol to unsubscribe from store when the ShadowElement is 
    // removed from the page.
    subscriberToken: Symbol;

    storeKey = 'default_count'

    // observedAttributes whitelist the attributes we wish to observe.
    // This allows us to have multiple count buttons in the store.
    static get observedAttributes() {
        return ['storekey']
    }

    constructor() {
        super();
        // Here we subscribe to store changes. 
        // Then we can compare the changes we want, 
        // and fully control how we update our component
        this.subscriberToken = store.subscribe((newState, oldState) => {
            if (newState.count !== oldState.count) {
                 const countButton = this.shadow.querySelector<HTMLButtonElement>('button');

                 if (countButton) {
                     countButton.innerText = this.getCountText();
                 }
            }
        })
    }


    add(amount: number) {
        store.setState({
            [this.storeKey]: (store.getState()[this.storeKey] || 0) + amount,
        })
    }

    getCountText() {
        let storeCount = store.getState()[this.storeKey];

        if (storeCount === undefined) {
            storeCount = 0;
            this.add(0)
        }

        return 'Count: ' + storeCount;
    }

    
    connectedCallback() {
        // An individual storeKey passed to the component: 
        // <my-bad-counter storekey="myKey"></my-bad-counter>
        // The component needs to be connected to the DOM before we can get the attribute,
        // which is why we do it in the connectedCallback method.
        this.storeKey = this.getAttribute('storekey') || this.storeKey;

        this.shadow.innerHTML = `
            <style>
                button {
                    background: hotpink;
                }
            </style>
            <button>${this.getCountText()}</button>
        `

        const countButton = this.shadow.querySelector<HTMLButtonElement>('button');
        countButton.onclick = () => this.add(1);
    }

    // If the storekey changes, we reload the entire component.
    attributeChangedCallback() {
        this.connectedCallback();
    }

    // disconnectedCallback is a lifecycle method of the native HTMLElement
    // It is run when the element is removed from the page.
    disconnectedCallback() {
        store.unsubscribe(this.subscriberToken);
    }
})

Better example

Here is an example of a better counter in typescript, where we utilize some of the different techniques used in the previous examples.

import { define, ShadowElement, Store } from "@ognaf/core";

const store = new Store<Record<string, number>>({});

// Services can be reused across multiple components, and it removes logic from the components, 
// making them a lot cleaner. A service can be individual service functions, a state holding class or an object.

const countService = {
    add: (storeKey: string, amount: number) => {
        store.setState({
            [storeKey]: (store.getState()[storeKey] || 0) + amount,
        })
    },

    getStoreCount: (storeKey: string) => {
        const storeCount = store.getState()[storeKey];

        if (Number.isInteger(storeCount)) {
            return storeCount;
        }

        countService.add(storeKey, 0)

        return 0;
    },

    getCountText: (storeKey: string) => {
        return 'Count: ' + countService.getStoreCount(storeKey);
    }
}

define('better-counter', class extends ShadowElement {
    subscriberToken: Symbol;

    countButton = document.createElement('button');

    storeKey = 'default_count'

    static get observedAttributes() {
        return ['storekey']
    }

    constructor() {
        super();

        this.subscriberToken = store.subscribe((newState, oldState) => {
            if (newState.count !== oldState.count) {
                this.countButton.innerText = countService.getCountText(this.storeKey);
            }
        })
    }
   
    connectedCallback() {
        this.storeKey = this.getAttribute('storekey') || this.storeKey;

        // Here we rewrite the shadowDOM. 
        this.shadow.innerHTML = `
            <style>
                button {
                    background: hotpink;
                }
            </style>
        `
        // Here we append the button instead of writing it to the template. 
        // This way we ensure that it's never undefined, and therefore we can avoid
        // a condition in the subscription method.
        this.shadow.appendChild(this.countButton)
        this.countButton.innerText = countService.getCountText(this.storeKey);
        this.countButton.onclick = () => countService.add(this.storeKey, 1);
    }

    attributeChangedCallback() {
        this.connectedCallback();
    }

    disconnectedCallback() {
        store.unsubscribe(this.subscriberToken);
    }
})