Gathering detailed insights and metrics for perlite
Gathering detailed insights and metrics for perlite
Gathering detailed insights and metrics for perlite
Gathering detailed insights and metrics for perlite
npm install perlite
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
29 Stars
34 Commits
1 Forks
2 Watching
1 Branches
1 Contributors
Updated on 19 Nov 2024
Minified
Minified + Gzipped
TypeScript (78.85%)
JavaScript (21.15%)
Cumulative downloads
Total Downloads
Last day
-70%
3
Compared to previous day
Last week
181.8%
93
Compared to previous week
Last month
57.1%
187
Compared to previous month
Last year
28.7%
1,962
Compared to previous year
2
22
hyperactiv 🌋 + lit-html ☄️ + extensions 🌊 = perlite 💎.
Perlite is a simple and declarative way to create rich client-side widgets designed with server-side apps in mind. Completely based on native/vanilla Javascript standards and doesn't require additional build steps or compilation. Plays well with server-side rendered apps and micro-frontends. For more details read the description.
Unlike the other frontend frameworks, eg. React, Vue, Angular or Svelte, which are mostly created for building SPA/RIA applications, Perlite's main goal is to make the life of developers of classical server-side applications a little bit easier and the modern front-end development techniques more accessible. Without extra knowledge of building tools and other dark sides of the frontend ecosystem . 👾
Perlite gives you a combination of the best ideas from the most popular SPA frameworks, like UI is a function of a state (React), reactive state driven development (Vue), observables (Angular) and lack of Virtual DOM (Svelte).
Perlite focuses on building standalone UI widgets placed across different parts of the server-generated page and provides handy tools to manage these widgets and interact between them.
Built on top lit-html - an efficient, expressive, extensible HTML templating library and hyperactiv - a super tiny reactive library. This means that your widgets will have a reactive state with direct object mutations, super-fast DOM updates, and low memory consumption.
The full bundle size of Perlite library is just 8.8Kb (min+gz). In addition, it's optimized for tree-shaking, so you can reduce the final size if not all features are used. At the same time, Perlite is full-featured enough to fulfill its purposes and no additional tools you needed in most of the cases.
1npm i --save perlite
or
1yarn add perlite
and use it
1import { html } from 'perlite';
If you are not using NPM, in modern browsers, you can import bundled ES module from CDN:
1import { html } from 'https://unpkg.com/perlite@latest/dist/perlite.min.mjs';
or
just add a regular script
-tag to your html for legacy browsers:
1<script src="https://unpkg.com/perlite@latest/dist/perlite.min.js"></script>
and use it via global namespace:
1const { html } = window.perlite;
dist/index.js
- UMD outputdist/index.mjs
- ESM outputdist/index.min.js
- UMD output (minified)dist/index.min.mjs
- ESM output (minified)dist/perlite.js
- IIFE bundledist/perlite.mjs
- ESM bundledist/perlite.min.js
- IIFE bundle (minified)dist/perlite.min.mjs
- ESM bundle (minified)sessionStorage
;Basically, the widget consists of two main parts: state
(object or function) and render
function. Use ES6 Template literals to describe your templates and tag them by special html
function imported from perlite
.
1import { html } from 'perlite'; 2 3export const state = { 4 name: 'world' 5}; 6 7export function render(state, emit) { 8 return html` 9 <h1>Hello ${state.name}</h1> 10 ` 11}
To create a new widget and append it to the page, import and call $
constructor function and pass the config object with several properties:
target
- DOM element where the widget will be rendered;state
- object or function representing the state of the widget;render
- a function representing a declarative template of the widget;For example, you can use ES6 Spread syntax to pass widget declaration exports to the constructor.
1import { $ } from 'perlite'; 2 3import * as HelloWorld from './widgets/HelloWorld.js'; 4 5export const $helloWorld = $({ 6 target: document.getElementById('helloWorld-widget'), 7 ...HelloWorld 8});
The constructor function will return an object which allows you to manage a widget. To distinguish widgets from regular JS objects, it's recommended to follow the naming convention by prefixing widget names with the “$” sign.
Actually, a widget object is just a namespace without any overall context. So, you can use ES6 Destructuring assignment and use things separately:
1const { 2 destroy, 3 render, 4 target, 5 state, 6 effect, 7 ctx, 8 on, 9} = $helloWorld;
Most often widget is a singleton, but in many cases, you need to use multiple widgets with the same declaration, but an isolated state. First of all, you need to use state
function, instead of an object, in widget declaration. This function should return a new state object, otherwise, state
will be shared between all widgets with the same declaration.
1export function state() { 2 return { 3 name: 'world' 4 }; 5}
After that, you can call $
function multiple times with the different targets
:
1import { $ } from 'perlite'; 2 3import * as HelloWorld from './widgets/HelloWorld.js'; 4 5export const $helloWorld1 = $({ 6 target: document.getElementById('helloWorld-widget-1'), 7 ...HelloWorld 8}); 9 10export const $helloWorld2 = $({ 11 target: document.getElementById('helloWorld-widget-2'), 12 ...HelloWorld 13});
When you deal with multiple widget instantiations, sometimes you want to work with them in the same manner. To do that, you can use the handy $$
container function to work with a bunch of widgets at once:
1import { $$ } from 'perlite'; 2 3import * as HelloWorld from './widgets/HelloWorld.js'; 4 5export const $$helloWorlds = $$({ 6 target: document.querySelectorAll('.helloWorld-widget'), 7 ...HelloWorld 8});
Please, use $$
(double “$” sign) prefix to visually distinguish widget containers from single widgets and regular JS objects.
Widgets and widget containers have mostly the same APIs, but having specifics at some points. For example, these functions will work the same for the end-user:
1$widget.on('eventName', () => { ... }); // add event listener for the widget 2$$widgets.on('eventName', () => { ... }); // add event listener for every widget in container 3 4$widget.render(); // re-render the widget 5$$widgets.render(); // re-render all widgets in container 6 7$widget.destroy(); // destroy widget 8$$widgets.destroy(); // destroy all widgets in container
The other APIs looks the same, but should be used differently:
1$widget.state.foo = 1; // directly change the state of the widget 2 3$$widgets.state(state$ => { // use it as a function with callback 4 state$.foo = 1; 5}); 6 7$widget.effect(() => { ... }); // add effect for the widget 8$$widgets.effect(state$ => () => { ... }); // add effect for every widget in container
Also, you can iterate through the container using forEach
:
1$$widgets.forEach(widget => { 2 // do your custom logic with each widget 3});
WIP
WIP
WIP
WIP
WIP
1html` 2 <h1>Title: ${title}</h1> 3`;
1html` 2 <h1>${title}</h1> 3 <h2>${a + b}</h2> 4 <h3>${user.name}</h3> 5 <h4>${description.substring(50)}</h4> 6 <h5>${formatDate(user.birthDay)}</h5> 7`;
1html` 2 <input value="${title}"> 3 <div class="default-class ${class}"></div> 4`;
1html` 2 <button ?disabled=${isDisabled}>Click me</button> 3`;
1html` 2 <input .value=${title}> 3`;
1html` 2 <input @input=${handleInput}> 3 <button @click=${e => alert('Clicked!')}>Click me</button> 4`;
1function render(state, emit) { 2 3 const welcomeMessage = html`<h1>Welcome ${state.user.name}</h1>`; 4 5 return html` 6 ${welcomeMessage} 7 <a href="/logout">Logout</a> 8 `; 9}
1function userInfo(user) { 2 return html` 3 <dl> 4 <dt>User name:</dt> 5 <dd>${user.name}</dd> 6 <dt>Email address:</dt> 7 <dd>${user.email}</dd> 8 <dt>Birthday:</dt> 9 <dd>${formatDate(user.birthDay)}</dd> 10 </dl> 11 `; 12} 13 14function render(state, emit) { 15 return html` 16 <h1>${state.title}</h1> 17 ${userInfo(state.user)} 18 `; 19}
in template
1html` 2 ${state.user ? 3 html` 4 <h1>Welcome ${state.user.name}</h1> 5 <a href="/logout">Logout</a> 6 ` : 7 html` 8 <a href="/login">Login</a> 9 ` 10 } 11`;
or in code
1function userMessage(user) { 2 if (user) { 3 return html` 4 <h1>Welcome ${user.name}</h1> 5 <a href="/logout">Logout</a> 6 `; 7 } else { 8 return html` 9 <a href="/login">Login</a> 10 `; 11 } 12} 13 14function render(state, emit) { 15 return html` 16 ${userMessage(state.user)} 17 `; 18}
in template
1html` 2 <ul> 3 ${state.items.map((item) => html` 4 <li>${item.title}</li> 5 `)} 6 </ul> 7`;
or in code
1function itemsList(items) { 2 return items.map((item) => html` 3 <li>${item.title}</li> 4 `); 5} 6 7function render(state, emit) { 8 return html` 9 <ul> 10 ${itemsList(state.items)} 11 </ul> 12 `; 13}
WIP
These events are pre-defined and emitted on target
node as the other widget custom events. In most cases, you should use the built-in on()
function, but you also can do-it-yourself and use the regular target.addEventListener()
function, but don't forget to remove when you don't need it.
mount
- fires once when the component has been first time rendered to the DOM;state
- fires on every state change, before DOM update;update
- fires on every DOM updated, after state
event;destroy
- fires once when the component is removed from the DOM;error
- fires on exception occur during the rendering cycle;Each life-cycle event, except error
, receives a model
of the widget in event.detail
. This state is not reactive and its changes won't trigger widget re-rendering. If you really need to start new DOM update cycle from a life-cycle event handler (basically, you shouldn't do that), you can use reactive state
or manual call render()
function via widget object.
In difference with the other life-cycle events, error
event receives an exception in event.detail
.
1$widget.on('update', e => { 2 console.log('Widget DOM updated. The current model is: ', e.detail); 3}); 4 5$widget.on('destroy', e => { 6 console.log('Widget is destroyed.'); 7}); 8 9$widget.on('error', e => { 10 console.error(e.detail.message); 11});
Just a reference to target node of a widget.
Reactive state of a widget based on initial state object (called model). Changing this state will perform a re-render and DOM updates.
1$widget.state.foo = 1; // widget scheduled for update 2$widget.state.bar = true; 3$widget.state.baz = 'horse'; // updates will be bunched
It's just a reference to plain state object, which is a model for reactive state (proxy target). You can changing this model, but because it's not reactive, re-rendering won't be performed. To apply these changes to the DOM you can use render()
function.
The effect is a function which executed each time its dependencies changed. Dependencies are tracked automatically and don't need to be explicitly specified.
1const cancel = $widget.effect(() => { 2 console.log('Foo is changed:', $widget.state.foo); 3}); 4... 5// somewhere latter 6cancel();
effect()
function is just a wrapper ontop of hyperactiv's computed()
with automatic dispose on widget destroy. So, you can use all things described in hyperactiv guide. This function return cancel()
function, so you can dispose of an effect when you actually don't need it.
This function lets you add an event listener to the widget to catch custom events dispatched by emit()
function and automatically removes the handler on widget destroy. Also, you can remove the handler manually using off()
function:
1const off = $widget.on('my-custom-event', (event) => { 2 console.log('Event payload', event.detail); 3}); 4... 5// somewhere latter 6off();
Call this function to manually re-render a widget. Usually, it's not necessary, because you need just use a state-driven approach and change the reactive state to automatically perform a re-render. But sometimes you may want to force the DOM update. This function is idempotent and safe to re-call. If actual state wasn't changed, no changes in DOM will performed.
Completelly destroy a widget, removes all event listeners and effects, and clean up the markup. It also fires a destroy
life-cycle event.
Calling this function is the most proper way to destroy the widget, but if, for some reason, the target
node will be removed from the DOM by external code, it will be tracked on the next render cycle, and destroy operations will be performed automatically.
This function receives callback function to get context values passed to the widget during creation.
1$widget.ctx((foo, bar, baz) => { 2 console.log('widget context values', foo, bar, baz); 3});
This function is fully synchronous and just returns the result of the callback. So you can return values you need directly or chain it with the other methods.
1const bar = $widget.ctx((foo, bar, baz) => bar); 2 3$widget.ctx((...ctx) => ctx).forEach((val) => ...);
More details about context.
WIP
Gets any widget by index in the order of target
list provided on the creation of the container.
The original list of targets
of the container widgets.
Works almost the same as on()
function of a single widget but add events listener to every widget inside the container.
Works almost the same as render()
function of a single widget but apply re-render to every widget inside the container.
Works almost the same as render()
function of a single widget but destroy all widgets inside the container.
Equal to ctx()
function of a single widget. All widgets inside the container share the same context values.
Use it for looping through all the widgets inside the container:
1$$widgets.forEach($widget => { 2 // do something with each widget 3});
Most often, you will need some initial widget state to be set by the server during page rendering. Just use data-attributes
on widget target
element and render necessary values there. For example, using PHP templating:
1<div 2 id="myWidget" 3 data-string="<?=$strVal?>" 4 data-number="<?=$numVal?>" 5 data-boolean="<?=$boolVal?>" 6 data-null="<?=$nullVal?>" 7 data-json="<?=$jsonVal?>" 8></div>
All data-attributes
that matched declared widget state (in widget declaration) will be picked up and applied to the widget during creation. Note: to be properly matched, attributes names should be in kebab-case, and widget state properties names should be in camelCase. For more info, check how kebabCase()
and camelCase()
functions works.
Regardless, that attribute values are always strings, some types will be automatically converted to the corresponding JS types (eg. boolean, number, null/undefined and even json. For more info, check how attrToVal()
function works.
Moreover, your external client-side code is also able to change data-attributes
of widget target node directly like this:
1const myWidgetTarget = document.getElementByID('myWidget'); 2myWidgetTarget.setAttribute('data-string', 'hello world');
and these changes will also be applied to the widget state at any moment it occurs and DOM will be triggered to update as well. It's strongly not recommended, but can be useful in cases when some part of your code doesn't have direct access to the widget object and its reactive state.
WIP
Perlite re-exports all lit-html built-in directives:
Follow the lit-html guide to lean how to use them.
WIP
1import { 2 capture, 3 passive, 4 once, 5 self, 6 stop, // stopPropagation() 7 prevent, // preventDefault() 8} from 'perlite'; 9... 10html` 11 <form @submit=${prevent(e => { ... })}> 12 ... 13 <button @click=${self(once(e => { ... }))}> 14 Submit 15 </button> 16 </form> 17`;
1import { ref } from 'perlite'; 2... 3html`<div @=${ref(el => state.el = el)}></div>`;
1import { decorator } from 'perlite'; 2... 3function myDecorator(node, foo, bar, baz) { 4 // do something 5 return { 6 update(foo, bar, baz) { ... }, 7 destroy() { ... } 8 }; 9} 10... 11html`<div @=${decorator(myDecorator, foo, bar, baz)}></div>`;
Directives are fully provided by lit-html without any specifics or limitations. So, you can use Creating directives section in lit-html guide to learn more about custom directive creation.
Basically, context
is just any additional arguments that can be passed to the widget's state
and render
functions on widget creation. These arguments can have any type and order you needed. The main thing you should know, context
values are static. They are passed through when the widget is created, their count and order can't be changed on all widget life-cycle. Of course, if some context
value is a reference to the object/array, its mutations could be applied in the next DOM update cycle. But, unlike the state
mutations, context mutations will never trigger a new DOM update cycle by itself.
1const context = { ... }; 2 3const $widget = $({ 4 target, 5 render, 6 state, 7 }, 8 context 9);
1const context1 = { ... }; 2const context2 = true; 3 4const $widget = $({ 5 target, 6 render, 7 state, 8 }, 9 context1, 10 context2, 11 ... 12);
1export function state(context1, context2) { 2 // change initial state model depending on context values 3 return { 4 ... 5 }; 6} 7 8export function render(state, emit, context1, context2) { 9 const something = Object.entries(context1).reduce(() => { ... }); 10 11 return html` 12 <div>Context2: ${context2}</div> 13 `; 14}
Basically, Perlite widgets designed without a focus on their composition. But sometimes you still need to insert one widget into a DOM tree of another widget and communicate with a nested widget on the rendering cycle of its "parent".
To do that, at first, you can create a target element of the nested widget in memory and use the context to pass this widget to render function of the parent.
1import * as Nested from './widgets/Nested.js'; 2import * as Wrapper from './widgets/Wrapper.js'; 3 4const $nested = $({ 5 target: document.createElement('div'), 6 ...Nested 7 }, 8); 9 10const $wrapper = $({ 11 target: document.getElementById('wrapper'), 12 ...Wrapper 13 }, 14 $nested 15);
After that, you can just use target
of the nested widget in template expression and it will be rendered as a fragment of the current widget's DOM tree. Very simple!
1export function render(state, emit, $nested) { 2 return html` 3 ${$nested.target} 4 5 <button @click=${e => $nested.state.count += 1}> 6 Increment nested value 7 </button> 8 `; 9}
The number of nested widgets, as well as the way they are passed through the context, is not limited in any way. You can choose the most appropriate way you like.
1const $wrapper = $({ 2 target: document.getElementById('wrapper'), 3 ...Wrapper 4 }, { 5 $nested1, 6 $nested2 7 } 8);
1export function render(state, emit, { $nested1, $nested2 }) { 2 return html` 3 ${$nested1.target} 4 <div> 5 ${$nested2.target} 6 </div> 7 `; 8}
Also known as store(s)
- a very simple observables
. Whenever a property of an observed object is changed, every function that depends on this property is called.
First of all, need to create a new observable
store:
1import { observe } from 'perlite'; 2 3export const store$ = observe({ 4 products: [], 5 config: {}, 6 user: {}, 7});
As it's often in Angular, use observables named with a trailing “$” sign to distinguish it from the other objects.
1import { computed, dispose } from 'perlite'; 2import { store$ } from './store.js'; 3 4const user$$ = computed(() => { 5 console.log('User data has changed', store$.user); 6}); 7... 8dispose(user$$);
To subscribe to the store properties, pass the callback function as a first argument of computed
function and just perform any operations or side-effects with needful properties. It also returns subscription
handler to dispose of a subscription if you no longer need it. It recommended to name subscriptions
with a trailing "$$" sign (double "$") for short and distinguish it from the other objects.
Dependencies are automatically tracked so you don't need to explicitly declare anything - just use the properties you need. Read more about these things in hyperactiv guide.
Just import the store to use it in a widget:
1import store$ from './store.js' 2... 3function userInfo(user) { ... } 4... 5function render(state, emit) { 6 return html` 7 <h1>${state.title}</h1> 8 ${userInfo(store$.user)} 9 `; 10}
The widget will be automatically updated when store values have changed.
WIP
WIP
Defer the code to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update:
1import { tick } from 'perlite'; 2 3$widget.state.foo += 1; 4 5await tick(); 6 7console.log('now DOM updated');
Or if it needed to perform some operation inside of render function/fragments that can trigger a rendering cycle again (which is most often not safe):
1import { tick, html } from 'perlite'; 2 3export function render(state, emit) { 4 5 // WRONG WAY - will trigger a new DOM update cycle 6 // before the current one is completed 7 state.result = ...; 8 9 // RIGHT WAY - defer state change to the next DOM update cycle 10 tick(() => { 11 state.result = ...; 12 }); 13 14 return html`...`; 15}
Creates and returns a new memoized version of the passed function that will cache the result based on its arguments.
1import { memo } from 'perlite'; 2 3function heavyFunc(foo, bar, baz) { ... } 4 5const funcMemoized = memo(heavyFunc); 6... 7funcMemoized(1, 2, 3); // executes a function, caches the result, and returns it 8... 9funcMemoized(1, 2, 3); // just a returns the result from cache 10funcMemoized(4, 5, 6); // new arguments - new execution 11funcMemoized(1, 2, 3); // still returns the result from cache
or use the second argument, function which provides custom cache invalidation logic:
1import { memo } from 'perlite'; 2 3function heavyFunc() { ... } 4 5const funcMemoized = memo(heavyFunc, (foo, bar, baz) => { 6 // decide whether to use the cached value or re-calculate the function 7 return true; 8});
Invalidation function should return true
to keep the cached result or false
to drop the cache, and re-execute the function. Also, it can return any unique value (string, number, even a reference) which will be used instead of arguments list to cache and retrieve the result.
1const funcMemoized = memo(heavyFunc, (foo, bar, baz) => { 2 return `${foo}-${bar}-${baz.quux}`; // this will be used as a cache identifier 3});
1import { attrToVal } from 'perlite'; 2 3attrToVal('false'); // false 4attrToVal('undefined'); // undefined 5attrToVal('null'); // null 6attrToVal('2'); // 2 7attrToVal('{"foo":1}'); // { foo: 1 }
1import { camelCase } from 'perlite'; 2 3camelCase('kebab-to-camel-case'); // kebabToCamelCase 4camelCase('kebab-to-pascal-case', true); // KebabToPascalCase
1import { kebabCase } from 'perlite'; 2 3kebabCase('camelToKebabCase'); // camel-to-kebab-case
Basically, Perlite is not really opinionated about how you should structure your projects. But to not leave you alone with this question, let's describe a possible project structure you may use.
So, the main project unit is a widget, and the main part of the widget is its declaration. That's why we suppose to create a widgets
folder to contain declarations of the project widgets. Any widget declaration can be a single file or subfolder for more complex widgets.
1./widgets/ 2 ./Widget1.js 3 ./Widget2/ 4 ./styles.css 5 ./index.js
Any widget can have fragments that are just re-usable pieces of the templates. You can keep fragments in the widget file if their number and size are not so big. Otherwise, you can take them out to a separate file or even a folder.
1./widgets/ 2 ./Widget1/ 3 ./fragments.js 4 ./index.js 5 ./Widget2/ 6 ./fragments/ 7 ./fragment1.js 8 ./index.js
Next thing that we have a widget creation process. In most cases, you'll need to create widgets right after DOM is ready and be able to get a widget object in any place of your code.
We suppose to add an index.js
file in the widgets
folder and create the widgets there. To get access to created objects you can just export it from this file. ES modules approach based on single instance pattern, so you'll be able to import widget objects in other files.
1./widgets/ 2 ./Widget1/ 3 ./Widget2/ 4 ./index.js
Your index.js
can look like this:
1import { $, $$ } from 'perlite'; 2 3// importing widget declarations 4 5import * as Widget1 from './Widget1.js'; 6import * as Widget2 from './Widget2.js'; 7 8// creating and exporting the widgets or widget containers 9 10export const $widget1 = $({ 11 target: document.getElementById('widget1Container'), 12 ...Widget1 13}); 14 15export const $$widget2 = $$({ 16 target: document.querySelectorAll('.widget2Container'), 17 ...Widget2 18});
After that, you'll be able to import any widget in any file of your project.
1import { $widget1 } from './widgets/'; 2 3$widget1.effect(() => { 4 // do something 5});
Regarding the stores, you may use almost the same approach - create a stores
folder with subfolders if needed, and hold stores in different files, optionally, re-exports them from a single entry point (index.js
).
1./stores/ 2 ./store1/ 3 ./store1-1.js 4 ./store1-2.js 5 ./index.js 6 ./store2.js 7 ./index.js
1import { html, bind, computed } from 'perlite'; 2import { user$ } from './stores/'; 3 4export const userName$$ = computed(() => { 5 console.log('user name is ', user$.name); 6}); 7 8export function fragment() { 9 return html` 10 <input value=${user$.name} @change=${bind(name => user$.name = name)}> 11 `; 12}
This software is licensed under the MIT © Pavel Malyshev.
No vulnerabilities found.
Reason
no binaries found in the repo
Reason
Found 0/30 approved changesets -- score normalized to 0
Reason
0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Reason
no SAST tool detected
Details
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
security policy file not detected
Details
Reason
license file not detected
Details
Reason
project is not fuzzed
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
37 existing vulnerabilities detected
Details
Score
Last Scanned on 2024-11-25
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More