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 🙏

© 2025 – Pkg Stats / Ryan Hefner

farinel

v2.5.1

Published

A clever and mischievous framework that makes your code dance with joy! As cunning as it is lightweight - because being smart doesn't mean being heavy!

Readme

Farinel

Farinel is a lightweight and reactive UI framework for creating user interfaces in JavaScript/TypeScript. It provides a declarative and functional approach to state management and component rendering, built on top of Ciaplu for pattern matching and state management.

Features

  • 🎯 Fully Reactive Pattern Matching - .when(), .with(), .withType() are all reactive (v2.5.0+)
  • 🎨 Native HTML components with TypeScript support
  • 🔄 Automatic DOM updates and efficient diffing
  • 🎭 Simplified event handling
  • 📦 Zero external dependencies
  • 🎨 Declarative state-based views
  • 🔄 State transformations with .extracting()
  • 🧪 State testing with .test()
  • 🎯 Pattern matching with Ciaplu's matchers
  • 🔍 Key-based diffing for optimal performance
  • 🎭 Type-safe component creation
  • ⚡ Input focus preservation during updates

Installation

npm install farinel

Quick Start

Basic Component

import { farinel } from 'farinel';
import { Div, Button } from './html';

const Counter = () => {
  const component = farinel()
    .stating(() => ({
      count: 0
    }))
    .otherwise(() => 
      Div({}, 
        Button({}, `Count: ${component.state.count}`)
          .on("click", async () => {
            await component.dispatch({
              count: component.state.count + 1
            });
          })
      )
    );

  return component;
}

Core Concepts

State Management

Farinel uses Ciaplu's pattern matching for state management. The state is managed through the stating() method:

const component = farinel()
  .stating(() => ({
    user: null,
    loading: false
  }));

State Updates (Dispatch)

State updates are handled through the dispatch() method:

await component.dispatch({
  user: { id: 1, name: 'John' },
  loading: true
});

Pattern Matching

Farinel exports all Ciaplu's pattern matching functions:

component
  .with({ type: 'success' }, () => 
    Div({}, "Success!")
  )
  .withType(Error, () => 
    Div({}, "Error occurred")
  )
  .when(state => state.count > 10, () => 
    Div({}, "Count is high!")
  )
  .otherwise(() => 
    Div({}, "Default view")
  );

State Transformations

Transform state before rendering:

component
  .stating(() => ({
    firstName: 'John',
    lastName: 'Doe'
  }))
  .extracting(state => ({
    ...state,
    fullName: `${state.firstName} ${state.lastName}`
  }))
  .otherwise(() => 
    Div({}, `Hello ${component.state.fullName}!`)
  );

Component Composition

Components can be composed and nested:

const UserProfile = ({ user }) => {
  const profile = farinel()
    .stating(() => ({
      user,
      editing: false
    }))
    .otherwise(() => 
      Div({}, 
        UserHeader({ user: profile.state.user }),
        UserDetails({ 
          user: profile.state.user,
          editing: profile.state.editing,
          onEdit: () => profile.dispatch({ editing: true })
        })
      )
    );

  return profile;
};

Event Handling

Events are handled with the on() method:

Button({}, "Submit")
  .on("click", async (e) => {
    e.preventDefault();
    await component.dispatch({
      loading: true
    });
    // ... handle submission
  });

Form Components

Farinel provides form components with state management:

const LoginForm = () => {
  const form = farinel()
    .stating(() => ({
      email: '',
      password: '',
      loading: false
    }))
    .otherwise(() => 
      Form({}, 
        Input({
          type: 'email',
          value: form.state.email,
          disabled: form.state.loading
        })
          .on("input", (e) => {
            // Update state directly to preserve focus
            form.state.email = e.target.value;
          }),
        Input({
          type: 'password',
          value: form.state.password,
          disabled: form.state.loading
        })
          .on("input", (e) => {
            // Update state directly to preserve focus
            form.state.password = e.target.value;
          }),
        Button({
          disabled: form.state.loading
        }, "Login")
          .on("click", async () => {
            // Dispatch only on submit, not on every keystroke
            await form.dispatch({ 
              ...form.state,
              loading: true 
            });
            // ... handle login
          })
      )
    );

  return form;
};

Root Creation

Create a root component and mount it to the DOM:

const app = farinel();
await app.createRoot(document.body, App);

State Observation

Observe state changes with the spy() method:

const stateChange = component.spy();
await component.dispatch({ count: 1 });
const newState = await stateChange;

Complete Example

import { farinel } from 'farinel';
import { Div, Button, Input, Form } from './html';

const LoginPage = () => {
  const loginPage = farinel()
    .stating(() => ({
      email: '',
      password: '',
      loading: false,
      error: null
    }))
    .when(state => state.error, () => 
      Div({}, 
        Form({}, 
          Div({}, `Error: ${loginPage.state.error}`),
          Input({
            type: 'email',
            value: loginPage.state.email,
            disabled: loginPage.state.loading
          })
            .on("input", (e) => {
              loginPage.state.email = e.target.value;
              loginPage.state.error = null;
            }),
          Input({
            type: 'password',
            value: loginPage.state.password,
            disabled: loginPage.state.loading
          })
            .on("input", (e) => {
              loginPage.state.password = e.target.value;
              loginPage.state.error = null;
            }),
          Button({
            disabled: loginPage.state.loading
          }, "Login")
            .on("click", async () => {
              await loginPage.dispatch({ 
                ...loginPage.state,
                loading: true 
              });
              try {
                // ... handle login
                await loginPage.dispatch({ 
                  ...loginPage.state,
                  loading: false,
                  error: null
                });
              } catch (error) {
                await loginPage.dispatch({ 
                  ...loginPage.state,
                  loading: false,
                  error: error.message
                });
              }
            })
        )
      )
    )
    .otherwise(() => 
      Div({}, 
        Form({}, 
          Input({
            type: 'email',
            value: loginPage.state.email,
            disabled: loginPage.state.loading
          })
            .on("input", (e) => {
              loginPage.state.email = e.target.value;
              loginPage.state.error = null;
            }),
          Input({
            type: 'password',
            value: loginPage.state.password,
            disabled: loginPage.state.loading
          })
            .on("input", (e) => {
              loginPage.state.password = e.target.value;
              loginPage.state.error = null;
            }),
          Button({
            disabled: loginPage.state.loading
          }, "Login")
            .on("click", async () => {
              await loginPage.dispatch({ 
                ...loginPage.state,
                loading: true 
              });
              try {
                // ... handle login
                await loginPage.dispatch({ 
                  ...loginPage.state,
                  loading: false,
                  error: null
                });
              } catch (error) {
                await loginPage.dispatch({ 
                  ...loginPage.state,
                  loading: false,
                  error: error.message
                });
              }
            })
        )
      )
    );

  return loginPage;
};

const App = async () => {
  const app = farinel();
  await app.createRoot(document.body, LoginPage);
};

App();

API Reference

Core Methods

  • stating(getState): Initialize component state
  • dispatch(newState): Update component state
  • createRoot(container, component): Mount component to DOM
  • spy(): Observe state changes
  • resolve(): Resolve component to final element

Pattern Matching Methods (from Ciaplu)

  • with(value, handler): Match exact value
  • withType(type, handler): Match by type
  • when(matcher, handler): Match by condition
  • otherwise(handler): Default handler
  • extracting(handler): Transform state
  • test(matcher): Test state condition

HTML Components

  • Div(attributes, children)
  • Button(attributes, children)
  • Input(attributes)
  • Form(attributes, children)
  • Select(attributes, children)
  • Option(attributes, children)

and more...

Best Practices

Conditional Rendering

Always use ternary operators with null for conditional rendering, not logical AND (&&) operators:

// ✅ CORRECT - Use ternary operator
state.showModal ? Div({}, "Modal content") : null

// ❌ WRONG - Using && can cause rendering issues
state.showModal && Div({}, "Modal content")  // Returns false when condition is false

Why? When using &&, JavaScript returns false when the condition is false. Farinel normalizes false to an empty string '', which corrupts the children array structure and breaks the diffing algorithm. Using ternary operators with : null ensures proper handling of conditional elements.

Input Focus Preservation

For text inputs, avoid calling dispatch() on every keystroke. Instead, update state directly:

// ✅ CORRECT - Direct state update preserves focus
Input({ value: component.state.text })
  .on("input", (e) => {
    component.state.text = e.target.value;  // No re-render
  })

// ❌ WRONG - Dispatch on every keystroke loses focus
Input({ value: component.state.text })
  .on("input", async (e) => {
    await component.dispatch({ text: e.target.value });  // Re-renders, loses focus
  })

Why? Calling dispatch() triggers a full re-render and diff/patch cycle. Farinel's PropsPatch automatically preserves input focus, but only if the input element is patched, not replaced. Direct state updates avoid unnecessary re-renders while typing.

Multi-Step Forms and Conditional Steps

Use unique key attributes for conditional elements to help Farinel's diffing algorithm:

state.step === 1 ? Div({ key: 'step-1' }, 
  // Step 1 content
) : null,

state.step === 2 ? Div({ key: 'step-2' }, 
  // Step 2 content
) : null

Why? Keys enable identity-based diffing, allowing Farinel to correctly track which elements are added, removed, or moved, rather than relying on positional matching.

License

MIT

Changelog

v2.5.0 (2025-11-27) - Reactive Pattern Matching 🚀

Major Feature:

  • Fully Reactive .when(), .with(), .withType(): All Ciaplu pattern matching methods are now reactive! When you call dispatch(), Farinel re-evaluates all patterns and automatically switches the rendered view based on the new state.

What This Means:

Before v2.5.0, only .otherwise() was reactive. Now you can use declarative pattern matching for state-based views:

const app = farinel()
  .stating(() => ({ status: 'loading' }))
  .when(state => state.status === 'loading', () => 
    Div({}, 'Loading...')
  )
  .when(state => state.status === 'success', () => 
    Div({}, 'Success!')
  )
  .when(state => state.status === 'error', () => 
    Div({}, 'Error!')
  )
  .otherwise(() => Div({}, 'Unknown'));

// This now works reactively!
await app.dispatch({ status: 'success' }); // UI updates to "Success!"
await app.dispatch({ status: 'error' });   // UI updates to "Error!"

Technical Details:

  • Patterns are evaluated in the order they were defined
  • First matching pattern wins
  • Falls through to .otherwise() if no pattern matches
  • Supports async predicates
  • Maintains full backwards compatibility

Test Coverage:

  • 8 comprehensive tests covering all pattern types
  • Tests for rapid state changes, event handlers, mixed patterns
  • All 61 existing tests continue to pass

Breaking Changes:

  • None - fully backward compatible with v2.3.x

v2.3.0 (2025-11-26)

Major Improvements:

  • Robust Null/Undefined Handling: Conditional rendering with null/undefined now uses comment placeholders (document.createComment("placeholder")) to maintain consistent DOM structure and prevent index mismatches during patching
  • Input Focus Preservation: Enhanced PropsPatch to automatically preserve input focus and cursor position during patches, eliminating focus loss issues
  • Key-Based Diffing: Implemented identity-based diffing using id and key attributes, enabling efficient updates for lists, conditional elements, and multi-step forms
  • Direct State Updates: Components can now update component.state directly without triggering re-renders, useful for form inputs and frequent state changes
  • Improved Conditional Rendering: Better handling of ternary operators and null children in the virtual DOM
  • Enhanced Patch Robustness: All patch types now gracefully handle missing or undefined elements common in conditional rendering scenarios

Breaking Changes:

  • None - fully backward compatible

Best Practices Added:

  • Use ternary operators (: null) instead of && for conditional rendering
  • Update input state directly to preserve focus during typing
  • Use key attributes for conditional/dynamic elements

v2.2.0

Key-Based Diffing:

  • Implemented identity-based diffing using id and key attributes
  • Automatic input focus and cursor position preservation in PropsPatch

v2.1.0

Auto-Wait Mechanism:

  • dispatch() now automatically waits for component mount before executing
  • Prevents "Element not found" errors when calling dispatch early in component lifecycle

v2.0.1+

Bug Fixes and Improvements

Nested Children Rendering (v2.0.1+)

Fixed a bug where nested arrays of children were not properly handled during rendering and patching. The virtual DOM now correctly flattens nested array structures in children:

// This now works correctly:
Div({}, 
  [[Button({}, 'A'), Button({}, 'B')], 
   [Button({}, 'C')]]  
);
// Renders as: <div><button>A</button><button>B</button><button>C</button></div>

What was fixed:

  1. Array Flattening: Children passed as nested arrays are automatically flattened to a single level
  2. Boolean Normalization: Boolean children (true/false) are converted to empty text nodes, consistent with render behavior
  3. Event Handling on Nested Elements: Event handlers now correctly attach to elements inside nested arrays
  4. Patching Consistency: The diff/patch algorithm now correctly compares and updates elements with normalized children structure

Event Handler Cleanup (Props)

Event handlers passed as props (e.g., onClick) that are removed during patch are now properly tracked and removed. The attachListener and detachPropListeners methods on Element ensure listeners don't leak.

Running Tests

To verify the fixes and run the test suite:

npm test

Test files covering these fixes:

  • src/__tests__/nested-events.test.ts - Nested array rendering and event handling
  • src/__tests__/events-update.test.ts - Event handler prop removal
  • src/__tests__/list-reordering.test.ts - List and array handling edge cases