A terse, attribute-driven JS library that provides reactive DOM updates within scoped components
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:
- 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).
- Minimal influence on dev structure. That means avoiding requirements like wrapping loop items or conditional output in singular root elements.
Pattr reads data from JSON script tags in your HTML:
<script id="p-root-data" type="application/json">
{
"title": "Welcome",
"count": 5
}
</script>Both data sources are merged and available throughout your app.
<div p-text="name"></div>
<div p-text="`Hello ${name}!`"></div><div p-html="htmlContent"></div>Security Note: Use with caution. Consider using the allow modifier to whitelist safe tags.
<span p-show="count > 5">Count is greater than 5</span>
<span p-show="!show_menu">Menu is hidden</span>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>String syntax:
<div p-style="show_menu ? 'max-height: 300px' : 'max-height: 0'"></div>Object syntax:
<div p-style="{ color: 'red', fontSize: '20px' }"></div>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>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
}"><input type="text" p-model="name" />
<textarea p-model="description"></textarea>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.
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-idattribute used to be required to usep-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.
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
Use p-for with <template> to iterate over data:
<template p-for="item of items">
<div p-text="item"></div>
</template><template p-for="[i, item] of items.entries()">
<div>
<span p-text="i"></span>: <span p-text="item"></span>
</div>
</template><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><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>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 extend directive functionality using colon syntax:
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 while preserving tags:
<div p-html:trim.100="htmlContent"></div>Whitelist specific HTML tags (removes all others):
<div p-html:allow.strong.em.p="unsafeHtml"></div>Chain multiple modifiers together:
<div p-html:allow.strong.em:trim.50="htmlContent"></div><!-- 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 install @plentico/pattrimport Pattr from '@plentico/pattr';<script src="/pattr.js" defer></script><!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>Pattr uses modern JavaScript features:
- Proxy
- Template literals
- Arrow functions
- Destructuring
- Spread operator
Supports all modern browsers (Chrome, Firefox, Safari, Edge).
MIT