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

@msinnes/dom

v0.0.20-alpha.0

Published

A dom library.

Downloads

6

Readme

@msinnes/dom

- A lightweight DOM rendering library.

At the top-level, the API is styled like ReactJS. There are class and function components along with hooks, but the API itself is much smaller. The Library itself is less than a fifth the size of React and React-Dom, and this library ports both functionality sets as a single unit. The library launches without an environment, so there is no need to import the library to use jsx.

- No intermediate DOM logic.

This library gives the user direct access to the DOM without an intermediate virtal DOM. Any props passed to a DOM element in JSX are the exact same props that will be passed to the DOM element when it renders. Just so we are clear, that includes setting the value innerHTML. This library does not hold your hand in the way other libraries do. I suggest you brush up on your html best practices and read up on the OWASP top 10.

- Part of a rendering Suite

@msinnes/dom is the core library of the @msinnes/dom* suite, and they are capable of quite a lot when working together. Front-end routing and state management are provided, and both can be tested and executed server side.

The Rendering Paradigm

- Structural Rendering

@msinnes/dom renders structurally. When an app renders, the input jsx is transformed to the DOM. During this process, a Virtual Application Tree is maintained based on the traversed jsx. As the application tree renders, any DOM nodes recognized along the way are written to the DOM.

Rather than doing a rigorous structural diff to determine if something needs to update or not, we traverse the entire application and update the DOM as we go. This adds the overhead for updating element props but removes the size and complexity of a diffing algorithm.

- Technical Explanation (Skip this if you want)

The rendering is based on this idea: Any subset of a set of tree nodes will form an implicit tree as long as the root node of the tree is present in the selected subset. JSX (JavaScript/XML) is a way of functionally expressing XML and HTML is an XML specification. Any XML paradigm implies a tree structure, so we can tie our Application and DOM Trees by using our Context API. Like the React Context API, this one refereces the nearest parent provided value. Using a DomParent Context, we can pick the HTML subtree from the Application tree. As we pick the DOM nodes, we can preserve the Application Node Ancestry in the subsequesnt DOM tree, which 'draws' a new tree. The shared root node and parent context allow us to write a full DOM tree on a single, pre-ordered traversal.

The real power of this algorithm can be expressed using a simple real-world example: script tags at the end of an html document. I ran into some intersting issues when I started cypress testing the API. If I wrote some jsx with a div element as the first child of the body element, it would follow that I should test that the first child of the body should be a div. Those tests failed because the saw a script tag. The script tag delivered as the last node of my HTML document was breaking my test. In most cases the script tag wound up as the first child of the document's body.

The algorithm broke the tests because it was appending to an empty list of children, but the browser doesn't see it that way. Instead of blindly appending to an array of children, the DOM context tracks nearest parent AND maintains a pointer to the current position in that parent's children. Because children can be nested, we have to track the position of nested parents, which means nested pointers up and down the context. Since I can write to the body element without having to account for script tags in the end-to-end tests, I know the algorithm tracks and updates correctly.

- Simple, Reader Friendly Explanation

JSX, or JavaScript XML, is a means for expressing html rather than writing mutations in vanilla JavaScript. This library treats jsx as an object with 3 properties, signature, props, and children. These three properties coincide with the the properties on an XML's abstract syntax tree: tag, attributes, and children. We deliver the application's representation of the DOM directly to the browser.

- Don't Be Lazy and Use This

This library does not abstract any logic, and it makes no assumptions on how or what you want to render to the DOM. For instance, React provides a very convenient onChange prop that will execute every time you perform an input on a form element. When you set the onchange prop on a DOM element, however, this is not the behavior. The onchange event handler only fires when an element loses focus. The correct event handler for handling user input is the oninput event. When implementing this library, there are no provided shortcuts. This allows us to keep the library small AND fast.

If I didn't scare you off with the Mozilla user documentation on input events, just wait. React tells you that setting the innerHTML prop is dangerous, and won't let you set that property directly. This is a What You See Is What You Get rendering library, and I will set any property you want on an element.

If you don't know why I am writing this warning here, then you need to study front end security safety measures before you continue programming in JavaScript.

The DOM Interface

Enter the DomRef -- A DomRef is the central building block of the dom rendering ecosystem. A DomRef wraps every DOM element and gives the application a means of interacting with the DOM. These objects lie at the center of the dom ecosystem, and provide the connection between user and DOM.

The DomRef logic in this library provides maximum flexibility for ref usage. Rather than providing an intermediate virtual ref that has to be...honestly I don't know how React tracks refs...no idea. We don't do whatever that is here. These refs are wrappers around the DOM element, and can be rendered in line as a JSX signature.

import { createRef } from '@msinnes/dom';

const Div = createRef('div');

createRef(document.body).render(<Div>Div Inner Text</Div>);
// document.body.innerHTML => <div>Div Inner Text</div>;

These refs are designed to take over the rendering ecosystem. Instead, these refs are used throughout the application to wrap elements, allowing for an extension of native rendering logic.

Usage

Install with your preferred package manager

With npm

npm install --save @msinnes/dom

With yarn

yarn add @msinnes/dom

You'll also need to install some dev dependencies to bundle and deliver the application. We will use Webpack and Babel for this example. While Webpack isn't required, Babel is the only supported Transpiler at this time. To use JSX you will need @msinnes/babel-preset-dom-jsx or you'll need to pair @msinnes/babel-plugin-dom-jsx with @babel/plugin-syntax-jsx.

The dev install command would like like this with npm:

npm install --save-dev @msinnes/babel-preset-dom-jsx @babel/cli babel-loader webpack webpack-cli

or with yarn:

yarn add -D @msinnes/babel-preset-dom-jsx @babel/cli babel-loader webpack webpack-cli

With this we have the minimum required libraries to bundle an @msinnes/dom application.

At the top level of the application, we'll need a .babelrc and a webpack.config.js.

.babelrc
{
  "presets": ["@msinnes/babel-preset-dom-jsx"]
}
webpack.config.js
module.exports = {
  entry: {
    main: './index.js',
  },
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.js?/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
    ],
  },
};

By adding a basic index.js we can show a small example of how to implement an @msinnes/dom application. The below application is a button with a click counter. The counter increments when the button is clicked.

index.js
import { createRef, useState } from '@msinnes/dom';

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <button
      onclick={() => setCount(count + 1)}
    >
      Click (Click'd: {count})
    </button>
  );
};

createRef(document.body).render(<App />);

API

The top-level method for rendering an application to the DOM. It takes two arguments, a jsx render and a DOM element. The element will act as a root node for the DOM portion of the input render. Any DOM elements processed during the render cycle will be rendered beneath the input element in the DOM tree.

- createRef

A factory function for creating DOM interfaces within the @msinnes/dom ecosystem. The factory takes either a string or a DOM element as an argument, and returns an instance of DomRef.

function createRef(elem: string | HTMLElement): DomRef;

The createRef function can take a string or an element, making this function the entrypoint for @msinnes/dom applications. First, create a dom ref from an existing dom node, and then render onto that ref.

import { createRef } from '@msinnes/dom';

createRef(document.body).render((
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
));
// document.body.innerHTML => <ul><li>Item 1</li><li>Item 2</li></ul>

A DomRef is allowed to take an existing HTML Element so users can render to a DOM element. In the above example, JSX will be rendered to the body element.

- Component

An abstract class that can be extended for creating class components. Making your own class components involves 2 steps. First, extending the Component class. Second, implementing a render method that returns the desired render.

import { renderApp, Component } from '@msinnes/dom';

class MyComponent extends Component {
  render() {
    return 'My DOM Text';
  }
}

renderApp(<MyComponent />, document.body);
// document.body.innerHTML => My Dom Text

**Technically, a render can be anything you want, but be careful. Unrecognized inputs render to the DOM as strings via the toString method.

Component Lifecycle

There are three component lifecycle methods exposed: componentDidMount, componentDidUpdate, and componentWillUnmount. The first two are instance effects, and fire after the render cycle is complete. componentWillUnmount fires during the render cycle, and executes immediately before the component is destroyed.

These instance methods can only be assigned to class components that extends the Component class. They also grant the user access to other instance methods. This means that the user can trigger a rerender after the render cycle completes. Careful use of this feature is necessary to prevent infinite loops.

- createContext

A factory function for the context API. The factory takes an optional initialValue as a parameter and returns a Context instance.

type Context = {
  Provider: FunctionComponent,
  Consumer: FunctionComponent,
};

function createContext(initialValue: *): Context;

Each context has a Provider and a Consumer. Can take an initial value, or that value can be set by a provider. Consumers will return the value of the nearest provider in the Application tree, or will return the initial value if no Provider is present in the tree. An initial value is not required.

- Providing Context Values

import { createContext } from '@msinnes/dom';

const { Provider } = createContext('initialValue');

const MyProvider = ({ myValue, children }) => (
  <Provider value={myValue}>
    {children}
  </Provider>
);

Values can be provided two ways, through a default value and through a provider implementation. In the above example, a wrapper component passes the data in the myValue prop to the Provider. This would cause all children beneath that provider to interpret the new value from the context.

- Class Component contextType

import { createRef, Component, createContext } from '@msinnes/dom';

const ctx = createContext('initialValue');

class MyComponent extends Component {
  static contextType = ctx;

  render() {
    return this.context;
  }
}

createRef(document.body).render(<MyComponent />);

Contexts can be accessed statically in class components. In this case, the text 'initialValue' would be rendered inside the body element.

- Function Component useContext

import { renderApp, useContext, createContext } from '@msinnes/dom';

const ctx = createContext('initialValue');

const MyComponent = () => {
  const value = useContext(ctx);

  return value;
};

renderApp(<MyComponent />, document.body);

Contexts can be accessed by the useContext hook in Function components. Like the class component example, this will render the text 'initialValue' inside of the body element.

- Context.Consumer

import { renderApp, createContext } from '@msinnes/dom';

const { Consumer } = createContext('initialValue');

const MyComponent = () => (
  <Consumer>
    {value => value}
  </Consumer>
);

renderApp(<MyComponent />, document.body);

The context Consumer component can be used to access a context value through inline jsx. The component's first child must be a function, and the first parameter of the function will be the context value. The function should then return the desired render. This case would render the same way as the prior examples.

- I will go ahead and say now that the consumer part of the api was developed based on the React model, but the Consumer component isn't a really nice way of getting these values. The longevity of this feature is up for debate.

Hooks

Hooks can be used in function components, and only function components. If you try and execute a hook outside of a rendering function component, it will throw an error. They are a specific piece of functionality, and they clearly have a defined workspace.

Hooks exist to optimize the use of Function Components in both this library and React. Function components with hooks are more expressive of functionality, and they are almost always capable of filling the place of a class component.

One note to keep in mind with hooks is that the same number of hooks need to execute in the same order. Any change in the number or order would cause massive issues internally. This means that conditional execution creates the potential for serious problems in your application. It is possible, but conditional execution of hooks is an absolute ANTIPATTERN . I'm not going to explain strategies to render hooks conditionally, but a keen mind could definitely accomplish it. Best of luck!

- useContext

Allows the use of Contexts in function components. Takes a context as an argument and returns the current value of that context.

function useContext(initialValue: *): *;

The current value is either the value provided by the nearest Provider in the Application tree, or it will be the initial value if there is no Provider present in the Application tree.

import { createRef, useContext, createContext } from '@msinnes/dom';

const ctx = createContext('initialValue');

const MyComponent = () => {
  const value = useContext(ctx);

  return value;
};

createRef(document.body).render(<MyComponent />);

- useEffect

Allows for implementing dom effects in function components. Dom effects are executed after updating the dom.

function useEffect(effectFn: Function, depArr: Array<*>): void;

Any function returned from an effect can by used for cleanup. Cleanup functions are executed after the component is removed from the dom.

const MyComponent = () => {
  useEffect(() => {
    // Store the original title value
    const originalTitle = window.title;
    window.title = 'MyComponent title';
    return () => {
      window.title = originalTitle;
    };
  });
  return 'myComponent render';
};

The above effect would update the title of the page on entry into the component, and reset the title when the component unmounts. This type of behavior is really useful for DOM interaction.

Effects will only run when the component is mounted to the DOM, but this will likely change to more closely resemble the React API. Effects can be re-executed by updating values in a dependency array.

const MyComponent = () => {
  useEffect(() => {
    performQuery(location.query);
  }, [location.query]);

  return 'MyComponent render';
};

The above component's effect will execute every time the location.query value changes.

- useMemo

Allows for memoizing information in a function component. This is best for information that doesn't need to be calculated on every render cycle.

function useMemo(memoFn: Function, depArr: Array<*>): *;

Information will only be calculated on the first run of the function component, but can be recalculated by updating an element of a dependency array.

const MyComponent = () => {
  const query = useMemo(() => {
    return parseQuery(location.query);
  }, [location.query]);

  return <MySuperSearchComponent query={query} />;
};

The above component is an example of parsing queryParams and passing them down into a search component. Every time location.query changes, the memo function will execute. Every time the memo function executes, the value returned from the hook will update. The data returned from the hook will be consistent on subsequent runs that don't re-execute the memo.

- useRef

Allows for a singleton DOM reference, usable in function components that lack an instance reference.

function useRef(elem: string | HTMLElement): DomRef;

The reference is memoized using the useMemo hook, and is only calculated when the component mounts. If the ref is then rendered to the dom via the function's returned render, it will be removed when the function component unmounts.

const MyLabelComponentWithHooks = () => {
  const Div = useRef('div');
  return <Div>Label Text</Div>
};

The ref can be used inline as if it was a class or function component.

- useState

Allows for use of state in function components. Returns a array with current state as the first element and a set function as the second element.

function useState(initialState): [currentState: any, setter: Function];

If the set function is executed, the state will be updated and the application will be rerendered.

const MyClickCounter = () => {
  const [count, setCount] = useState(0);
  return (
    <button type="button" onclick={() => setCount(count + 1)}>
      Click: {count}
    </button>
  );
};

The above component will increment a counter and display the value when a button is clicked. The count will be set to 0 initially, but it will increment every time the button is clicked. If the button is clicked twice, its text would read Click: 2.

Implementing without JSX


The following methods can be used to render an application without JSX. There are some other cases when these methods can be used to keep logic pure (not mutating elements when invoking the children prop is one example).

- createElement

A method that will output a valid dom render object from an input signature, props, and children. It's a function helper for outputting the same thing as a transpiled JSX file.

const createElement = (signature, props, children) => {
  return {
    signature: signature,
    props: props || {},
    children: children || [],
  };
};

That's pretty much it. Super simple helper.

- cloneElement

A method that takes an input element and outputs a new dom render object. If props are passed, those props will be spread on to the output render object. If children are passed as a third argument, they will replace any children on the cloned object.

const cloneElement = (
  { signature, props = {}, children }, // input component
  nextProps = {},
  nextChildren,
) => {
  return {
    signature,
    props: {
      ...props,
      ...nextProps,
    },
    children: nextChildren || children || [],
  };
};

Another super simple helper copied straight from the codebase.

A Note About Renders

I created the above helper functions because writing out all of the component objects is very verbose and labor intensive; however, you can completely skip JSX and these helper functions by just writing all of your renders as objects with signature, props, and children properties.

const render = {
  signature: 'ul',
  props: {},
  children: [
    {
      signature: 'li',
      props: {},
      children: ['Item 1'],
    },
    {
      signature: 'li',
      props: {},
      children: ['Item 2'],
    },
  ],
};

This is what JSX looks like when it transpiles. That said, anything can be a valid render in this specification. Things like empty strings, null, and undefined won't render anything to the DOM. Strings and Arrays are valid inputs. Strings render to the DOM as content, and arrays (interchangeable with JSX fragments) give us a means of grouping components.

Now, let's say you were to pass a Date instance into the render function. If the renderer comes across something it doesn't recognize, then the engine will call toString on the object. The string output is then rendered to the DOM.