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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@plentico/pattr

v0.0.22

Published

A lightweight reactive framework

Downloads

86

Readme

Pattr

A terse, attribute-driven JS library that provides reactive DOM updates within scoped components

Purpose

This is a companion project to Pico. It's a simple JS script that interacts with "p" attributes on HTML markup to provide reactive updates.

It's a similar concept to AlpineJS, but the main differences are that AlpineJS does way more, and Pattr has an intentionally compact syntax and scopes components so that:

  • Parent changes update Child and Grandchild components
  • Child changes update Grandchildren, but do nothing to Parents
  • Grandchildren do not impact Child or Parents

Goals:

  1. Modify the markup as little as possible. That means, simple, readable syntax and making inferences from standard html (e.g. an input with type=number automatically converts string value to number).
  2. Minimal influence on dev structure. That means avoiding requirements like wrapping loop items or conditional output in singular root elements.

Table of Contents

Data Sources

Pattr reads data from JSON script tags in your HTML:

Root Data (Props from CMS)

<script id="p-root-data" type="application/json">
{
    "title": "Welcome",
    "count": 5
}
</script>

Both data sources are merged and available throughout your app.

Directives

p-text - Display Text Content

<div p-text="name"></div>
<div p-text="`Hello ${name}!`"></div>

p-html - Display HTML Content

<div p-html="htmlContent"></div>

Security Note: Use with caution. Consider using the allow modifier to whitelist safe tags.

p-show - Conditional Display

<span p-show="count > 5">Count is greater than 5</span>
<span p-show="!show_menu">Menu is hidden</span>

p-show:pre-scope - Pre-Scope Conditional Display

For use with wrapped components (e.g., from Pico templating), evaluate visibility before p-scope runs:

<!-- From Pico: {if age > 10} wraps a component that has p-scope -->
<div p-show:pre-scope="age > 10" p-scope="double = age * 2">
    Your doubled age is: <span p-text="double"></span>
</div>

This ensures the conditional from the parent template (age > 10) is evaluated using the parent scope before the component's p-scope executes. Without this, the condition would be evaluated after scoping, which may not be the desired behavior for conditional wrapper components.

You can combine both for complex scenarios:

<!-- Pre-scope: evaluated before p-scope (parent scope context) -->
<!-- Regular p-show: evaluated after p-scope (scoped context) -->
<div 
    p-show:pre-scope="age > 10" 
    p-show="double > 30" 
    p-scope="double = age * 2"
>
    Your doubled age (<span p-text="double"></span>) is greater than 30
</div>

p-style - Set Styles

String syntax:

<div p-style="show_menu ? 'max-height: 300px' : 'max-height: 0'"></div>

Object syntax:

<div p-style="{ color: 'red', fontSize: '20px' }"></div>

p-class - Set Classes

String syntax:

<div p-class="active"></div>

String syntax with template literals

<div p-class="`base-class ${isActive ? 'active' : ''} ${hasError ? 'error' : ''}`">

Array syntax:

<div p-class="['btn', 'btn-primary']"></div>

Object syntax (conditional classes):

<div p-class="{ active: isActive, disabled: !isEnabled }"></div>

Static Class Merging

By default, p-class preserves static classes defined in your HTML and merges them with dynamic classes:

<div class="card animate" p-class="{ 'highlight': isActive }">
  • card and animate are preserved (static classes not in p-class)
  • highlight is added/removed based on isActive
  • Result: "card animate" or "card animate highlight"

Class Collision Handling

If a static class has the same name as a dynamic class, p-class takes precedence and manages it:

<div class="item collapsed" p-class="{ 'collapsed': !isExpanded }">
  • item is preserved (no collision)
  • collapsed is managed by p-class (collision - p-class wins)
  • When isExpanded=true: class is "item" (collapsed removed)
  • When isExpanded=false: class is "item collapsed"

p-class:replace - Replace All Classes

Use the :replace modifier to completely replace all classes (old behavior):

<!-- Without :replace - merges with static classes -->
<div class="static-class" p-class="themeClass">
<!-- Result: "static-class dark-theme" -->

<!-- With :replace - replaces all classes -->
<div class="static-class" p-class:replace="themeClass">
<!-- Result: "dark-theme" (static-class removed) -->

This is useful when you want p-class to have complete control over the element's classes:

<div class="old-theme" p-class:replace="{ 'light-theme': isLight, 'dark-theme': !isLight }">

p-attr - Set Attributes

Single attributes

<div 
  p-attr:data-id="userId" 
  p-attr:data-total="price * quantity" 
  p-attr:data-name="firstName + ' ' + lastName"
  p-attr:data-age="`${name} is ${age} years old`"
  p-attr:data-status="isActive ? 'active' : 'inactive'"
  p-attr:data-upper="userName.toUpperCase()"
  p-attr:aria-label="ariaText"
  p-attr:href="linkUrl"
>

Object syntax for multiple attributes

<div p-attr="{ 
  'data-id': userId, 
  'data-total': price * quantity,
  'data-name': firstName + ' ' + lastName,
  'data-age': `${name} is ${age} years old`,
  'data-status': isActive ? 'active' : 'inactive',
  'data-upper': userName.toUpperCase(),
  'aria-label': ariaText,
  'href': linkUrl 
}">

p-model - Two-Way Data Binding

<input type="text" p-model="name" />
<textarea p-model="description"></textarea>

Event Handling

Use p-on:eventname to handle events:

<!-- Click events -->
<button p-on:click="count++">Increment</button>
<button p-on:click="count--">Decrement</button>
<button p-on:click="items = [...items, 'New Item']">Add Item</button>

<!-- Input events -->
<input p-on:focus="isEditing = true" p-on:blur="isEditing = false" />

<!-- Multiple statements -->
<button p-on:click="count++; show_menu = false">Update & Close</button>

Available events: click, focus, blur, input, change, submit, keydown, keyup, mouseenter, mouseleave, etc.

Scoped Components

Create nested components with isolated reactive state using p-scope (and optionally p-id):

<div>
    Parent count: <span p-text="count"></span>
    <button p-on:click="count++">+</button>
</div>

<section p-id="child1" p-scope="count = count * 2; name = name + 'o';">
    <div>Child count (×2): <span p-text="count"></span></div>
    <button p-on:click="count++">+</button>
    
    <section p-scope="count = count + 1;">
        <div>Grandchild count (+1): <span p-text="count"></span></div>
        <button p-on:click="count++">+</button>
    </section>
</section>

The p-id attribute used to be required to use p-scope, now it's optional and can be omitted entirely. It's still available because it could be helpful for debugging purposes, migrating data between elements, or if external tools/scripts need to reference scopes by name.

Scoping Example

Components down the chain can diverge from the reactivity provided by their Parent components. However, if a Parent component is updated, it will resync all descendant components.

Parent = 2
Child (Parent * 2) = 4
Grandchild (Child + 1) = 5

If we increment Parent by 1:

Parent = 3
Child (Parent * 2) = 6
Grandchild (Child + 1) = 7

If we now increment Child by 1 (diverges from Parent):

Parent = 3
Child (Parent * 2) = 7
Grandchild (Child + 1) = 8

But if we then increment Parent by 1 (resyncs with Parent):

Parent = 4
Child (Parent * 2) = 8
Grandchild (Child + 1) = 9

Loops

Use p-for with <template> to iterate over data:

Simple Iteration

<template p-for="item of items">
    <div p-text="item"></div>
</template>

With Index (Array Destructuring)

<template p-for="[i, item] of items.entries()">
    <div>
        <span p-text="i"></span>: <span p-text="item"></span>
    </div>
</template>

Nested Loops (BROKEN, STILL WIP)

<template p-for="[i, cat] of cats.entries()">
    <div>
        <h3 p-text="cat"></h3>
        <template p-for="letter of cat">
            <button p-on:click="cats[i] = cat + letter" p-text="letter"></button>
        </template>
    </div>
</template>

Loop Actions

<template p-for="[i, item] of items.entries()">
    <div>
        <span p-text="item"></span>
        
        <!-- Remove item -->
        <button p-on:click="items = items.filter((_, idx) => idx !== i)">Remove</button>
        
        <!-- Update item -->
        <button p-on:click="items[i] = items[i] + '!'">Add !</button>
    </div>
</template>

<!-- Add new item -->
<input p-model="new_item" />
<button p-on:click="items = [...items, new_item]; new_item = ''">Add</button>

SSR Support

Pattr supports hydrating server-side rendered loops. Add p-for-key attributes to SSR elements:

<template p-for="item of items">
    <div>...</div>
</template>

<!-- SSR rendered elements -->
<div p-for-key="0">Apple</div>
<div p-for-key="1">Banana</div>
<div p-for-key="2">Orange</div>

Modifiers

Modifiers extend directive functionality using colon syntax:

Sync Scope (2-Way Binding)

Use :sync with p-scope to enable bidirectional data flow between parent and child scopes. Changes to local variables propagate back to the parent:

<div>
    Parent count: <span p-text="count"></span>
    <button p-on:click="count++">+</button>
</div>

<!-- With :sync, changes to childCount update parent count too -->
<section p-scope:sync="childCount = count">
    <div>Synced count: <span p-text="childCount"></span></div>
    <button p-on:click="childCount++">+ (updates parent)</button>
</section>

<!-- Without :sync (default), changes stay local -->
<section p-scope="localCount = count">
    <div>Local count: <span p-text="localCount"></span></div>
    <button p-on:click="localCount++">+ (stays local)</button>
</section>

Works with multiple variables and computed expressions:

<section p-scope:sync="x = a; y = b * 2">
    <input p-model="x" /> <!-- Updates parent 'a' -->
    <input p-model="y" /> <!-- Updates parent 'b' -->
</section>

Trim Text

Limit text length and add ellipsis:

<div p-text:trim.50="longText"></div>
<!-- Result: "This is a very long text that will be tr..." -->

Trim HTML

Trim HTML while preserving tags:

<div p-html:trim.100="htmlContent"></div>

Allow HTML Tags

Whitelist specific HTML tags (removes all others):

<div p-html:allow.strong.em.p="unsafeHtml"></div>

Combine Modifiers

Chain multiple modifiers together:

<div p-html:allow.strong.em:trim.50="htmlContent"></div>

Installation

CDN

<!-- Via unpkg -->
<script src="https://unpkg.com/@plentico/pattr" defer></script>

<!-- Via jsdelivr -->
<script src="https://cdn.jsdelivr.net/npm/@plentico/pattr" defer></script>

<!-- Minified -->
<script src="https://unpkg.com/@plentico/pattr/min" defer></script>

npm

npm install @plentico/pattr
import Pattr from '@plentico/pattr';

Local

<script src="/pattr.js" defer></script>

Complete Example

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/@plentico/pattr" defer></script>
    <script id="p-root-data" type="application/json">
    {
        "title": "Todo App",
        "username": "John"
    }
    </script>
    <script id="p-local-data" type="application/json">
    {
        "todos": ["Buy groceries", "Walk dog"],
        "new_todo": "",
        "show_completed": false
    }
    </script>
</head>
<body>
    <h1 p-text="title"></h1>
    <p>Welcome, <span p-text="username"></span>!</p>
    
    <!-- Add todo -->
    <input p-model="new_todo" placeholder="New todo..." />
    <button p-on:click="todos = [...todos, new_todo]; new_todo = ''">
        Add Todo
    </button>
    
    <!-- Todo list -->
    <template p-for="[i, todo] of todos.entries()">
        <div>
            <span p-text="todo"></span>
            <button p-on:click="todos = todos.filter((_, idx) => idx !== i)">
                Delete
            </button>
        </div>
    </template>
    
    <!-- Scoped component -->
    <section p-scope="count = todos.length;">
        <p>Total todos: <span p-text="count"></span></p>
    </section>
</body>
</html>

Browser Support

Pattr uses modern JavaScript features:

  • Proxy
  • Template literals
  • Arrow functions
  • Destructuring
  • Spread operator

Supports all modern browsers (Chrome, Firefox, Safari, Edge).

License

MIT