Gathering detailed insights and metrics for rimmel
Gathering detailed insights and metrics for rimmel
Gathering detailed insights and metrics for rimmel
Gathering detailed insights and metrics for rimmel
A Functional-Reactive UI library for the Rx.Observable Universe.
npm install rimmel
Typescript
Module System
Node Version
NPM Version
72.7
Supply Chain
99.4
Quality
89
Maintenance
100
Vulnerability
100
License
TypeScript (96.95%)
JavaScript (2.69%)
HTML (0.3%)
Shell (0.06%)
Total Downloads
3,269
Last Day
9
Last Week
19
Last Month
255
Last Year
2,781
31 Stars
252 Commits
4 Forks
3 Watching
11 Branches
3 Contributors
Minified
Minified + Gzipped
Latest Version
1.3.1
Package Id
rimmel@1.3.1
Unpacked Size
967.96 kB
Size
240.66 kB
File Count
600
NPM Version
8.19.3
Node Version
18.13.0
Publised On
09 Dec 2024
Cumulative downloads
Total Downloads
Last day
800%
9
Compared to previous day
Last week
-24%
19
Compared to previous week
Last month
-7.9%
255
Compared to previous month
Last year
616.8%
2,781
Compared to previous year
Rimmel is a small, lightweight and fast UI library entirely built around the Observable and Observer patterns, where:
Writing reactive streams is easier than you think: take an input, transform it, return it. If you need a crash-course on reactive streams, tailored for UI development, check this one out.
The "Hello World" for reactive interfaces is the click counter: one button, you click it, he counts it.
This is how it works:
The click
event is "sourced" into counter
, which is an Observable Subject that takes Event
objects and emits numbers.
The result is then "sinked" into the <span>
element at the end.
Most JavaScript UI libraries and frameworks are designed for the Imperative Programming paradigm. Occasionally they may also support some aspects of Functional/Functional-Reactive Programming. Third-party adapters or utility libraries are often used to help with all this but the truth is that the functional-reactive paradigm was just an afterhthought and its use is severely limited, inconvenient or plain awkward to apply in practice.
Rimmel focuses primarily on the functional-reactive paradigm (FRP, for short), to make it "just work"!
What that means in practice is you treat nearly everything as a stream — data in, data out.
You don't write code to change the state of something else, as in:
1// The imperative way 2target.property = value; 3target.setProperty('prop1', value);
What you do instead, is you connect both ends of your stream to your component's HTML, within the template:
1const ClickCounterComponent = () => { 2 // this takes click events in input 3 // and emits numbers in output 4 const count = new BehaviorSubject(0).pipe( 5 scan(x=>x+1) 6 ); 7 8 return rml` 9 <button onclick="${count}">click me</button> 10 11 Count: <span>${count}</span> 12 `; 13};
The concept of Virtual DOM originates from the assumption that the DOM is slow, which might be the case for some frameworks that make large numbers of unnecessary updates or re-renders, in which case it may become less expensive to run those computations outside of the DOM.
Since Rimmel never makes any unnecessary DOM update and the concept of a component re-rendering doesn't exist by itself, the whole idea of a Virtual DOM is simply not needed.
Rimmel uses Sinks, which are memory and/or performance-optimised direct DOM manipulation functions, attached to the Promises or Observable streams you provide. When there is new data, it gets sinked to the DOM. You are in control of when or how often you emit data, so you can also throttle/sample/debounce, use any schedulers as you see fit for your needs. RxJS provides all that, so if you know RxJS, you're ready to make the most of Rimmel.
Several UI libraries and frameworks around have a concept of component re-rendering, which implies running the whole component's function again in order to check its output and diff it against the Virtual DOM. Since Rimmel makes use of true reactive streams for state management, everything becomes dramatically simpler.
There is only one time a component is "rendered" and that is when it first lands on the page. After that, only "updates" happen.
If you need to render something based on a condition, plain-old standard JS can help you. No need for further abstractions:
1document.body.innerHTML = rml` 2 <main> 3 ${ 4 condition == true 5 ? rml`<div>yes, it's true</div>` 6 : rml`<div>no, it's false</div>` 7 } 8 </main>
You only want to use this method if your condition is static, and you don't need to reflect changes over time.
If you need to render based on a live, ever-changing condition, you can just use a normal Observable stream:
1// Use RxJS's `map` operator to emit any HTML based on whatever conditions you like 2const conditionalStream = new BehaviorSubject(0).pipe( 3 scan(x=>x+1), 4 map(data => { 5 if(data &1) { 6 return rml`<span>${data}, is odd</span>` 7 } else { 8 return rml`<span>${data}, is even</span>` 9 } 10 }) 11); 12 13document.body.innerHTML = rml` 14 <div>Count: ${conditionalStream}</div> 15 <button onclick="${conditionalStream}">click me</button> 16`;
Forward refs are a construct used in Imperative UI libraries to enable referencing and later modifying DOM elements that don't exist yet (you're writing components that will act on DOM elements... after they are mounted).
Rimmel enables you to declare future changes to a DOM element by means of a simple Promise or Observable. The mounting and data binding is completely managed by Rimmel. This means when they resolve or emit, the elements will already be there to receive them, effectively making the use of Forward Refs redundant.
When you're dealing with structured data, like lists or grids of repeated data, the best way to handle it depends on multiple factors, so it's left as an extension opportunity.
You can create your custom, advanced Sinks to render data structures of any complexity and manage any specific aspects of its interactions in the most optimal way for your needs. This is a perfect case for reactive grids, spreadsheets, SVG or Canvas drawings, interactive 3D scenes, etc.
To display and manage the UI for any dynamic list where you plan to support CRUD operations, perhaps drag'n'drop, you may consider using a Collection
from ObservableTypes, which is a rendering-aware extension of Array
, every method of which is mapped to an optimised UI rendering command, and it also exposes the Observable
and Observer
interfaces for seamless reactivity and integration with Rimmel and RxJS.
Want a little more involved example of stream usage?
Let's make a component featuring a Red, Green and Blue slider that get transformed into an #RRGGBB colour string, displayed in a text box and used as the fill colour of an SVG circle:
Here is the corresponding code:
1const toHex = n => n.toString(16).padStart(2, '0'); 2const toRGBString = rgbArr => `#${rgbArr.map(toHex).join('')}`; 3const toNumericStream = x => new Subject().pipe( 4 map(e => parseInt(e.target.value, 10)), 5 startWith(x), 6); 7 8const ColorPicker = (initial = [0, 0, 0]) => { 9 const [R, G, B] = initial.map(toNumericStream); 10 const RGB = combineLatest([R, G, B]).pipe( 11 map(toRGBString), 12 ); 13 14 return rml` 15 R: <input type="range" value="${initial[0]}" oninput="${R}"> 16 G: <input type="range" value="${initial[1]}" oninput="${G}"> 17 B: <input type="range" value="${initial[2]}" oninput="${B}"> 18 19 Current <span>${RGB}</span> 20 21 <svg viewbox="0 0 40 40" width="40" height="40"> 22 <circle fill="${RGB}" cx="20" cy="20" r="20" /> 23 </svg> 24 `; 25} 26 27document.getElementById('root-node').innerHTML = ColorPicker([255, 128, 64])
As you can see, there are three main streams, one for each colour slider in the HTML.
When they emit, their values are merged together through combineLatest
, which passes them through as an array to toRGBString
which will return the string as we need it.
Finally, we have two sinks where the data ends up; one as the innerHTML of the , ther other as the fill colour of the SVG shape.
"State", as the word itself suggests, is something static, so it doesn't belong to the dynamic, interactive, reactive webapps we make every day.
The rationale is that "state", as represented by plain old values such as numbers, strings and objects that are stored somewhere in memory is something you almost never need to read. Not now, not in 2 seconds, not in 45 minutes, not tomorrow. You only need those when certain events happen, in order to respond.
After that, everything should go quiet, including your CPU, to keep your laptop cool until the next UI event occurs.
This is, in summary, the discrete-functional-reactive paradigm behind Observables and RxJS (as opposed to the functional-reactive paradigm in general in which state is more like a continuous flow of data).
Event-driven reactivity as modelled by Observables is therefore the perfect way to describe state as it changes through the lifetime of an application at the occurrence of various discrete UI events.
Modelling your state as one or more observable streams will give you fine-grained control over async events and their coordination, thanks to the full range of RxJS operators you can use.
All Rimmel does is binding your observable streams to the UI in a seamless integration that will result in improved code quality, scale, testability and performance.
Component lifecycle events such as onmount
, beforeunmount
, present in most other imperative frameworks quickly become useless, redudant and discouraged here.
Streams and other listeners get connected and disconnected automatically for you when a component is mounted/unmounted. If you think about it, this is exactly what you would normally do in your init/onmount functions, so you no longer have to deal with these tiny details.
Since you only declare streams now and let Rimmel connect them to the DOM and each-other, your code will be immensely more concise, cleaner and more testable.
Rimmel still has a rml:onmount
event, but its use is only left as a last resort to integrate imperative, non-Rimmel components (some old jQuery plugins, etc?)
You may have already realised that writing UI components with Rimmel means you no longer have to deal with side effects, which makes it trivial to make your code purely functional.
There are two key concepts used by Rimmel: sources and sinks.
Sources are things that generate data which you can optionally process and transform along the way. What remains goes somewhere. That somewhere is usually referred to as a sink.
Sources typically include any DOM events such as onclick
or onmousemove
, fetch()
calls, just like promises in general, async functions and, most notably, Observables.
Sinks are most often the place where you want to display any information in the UI. Your main document, some HTML element, etc.
With RML/Rimmel you can treat most DOM elements as sources, sinks, or both.
Rimmel supports event native and synthetic listeners from all DOM elements. All Observers such as event listener functions, Subjects and BehaviorSubjects will receive events as emitted by the DOM.
1// Observable Subjects 2const stream = new Subject<MouseEvent>(); 3target.innerHTML = rml`<button onclick="${stream}></button>`; 4 5 6// Plain functions 7const fn = (e: MouseEvent) => alert('hover'); 8target.innerHTML = rml`<a onmouseover="${fn}></button>`; 9 10// Event Delegation 11const buttonClicks = new Subject<MouseEvent>().pipe( 12 filter(e => e.target.tagName == 'BUTTON') 13); 14 15target.innerHTML = rml` 16 <div onclick="${buttonClicks}> 17 <button>button1</button> 18 <button>button2</button> 19 <button>button3</button> 20 </div> 21`;
In normal circumstances your event handlers receive a native DOM Event
object, such as MouseEvent
, PointerEvent
, etc.
To enable a better separation of concerns, as of Rimmel 1.2 you can use Event Adapters to feed your event handlers or Observable streams the exact data they need, in the format they expect it, rather than the generic, raw DOM Event objects.
Do you only need the relative [x, y]
mouse coordinates when hovering an element?
Use <div onmousemove="${ OffsetXY(handler) }">
Do you want the last typed character when handling keyboard events?
Use <input oninput="${ Key(handler) }">
Rimmel comes with a handful of Event Adapters out of the box, but you can create your own with ease. Event Adapters become particularly useful when you have a main data stream that you want to feed from different elements of different types that emit different events. Using an adapter for each, you can make sure you always get data in a unified format in your main data stream.
In certain cases it can be useful to have an Event Adapter made of multiple steps. Adapter Operators are simple RxJS operators that make it possible to create whole event adapter pipelines to better suit mode advanced needs.
If you know how to use the pipe()
function from RxJS, then you almost know how to use source(...operators, target)
from Rimmel.
It works like pipe()
, except it applies the same operators to data coming in, rather than going out of an Observable stream: source(...operators, targetObserver)
1import { rml, source } from 'rimmel'; 2 3const datasetValue = map((e: Event) => Number(e.target.dataset.value)); 4const buttonClick = filter((e: Event) => e.target.tagName == 'BUTTON'); 5 6const Component = () => { 7 const total = new Subject<number>().pipe( 8 scan((a, b) => a+b, 0) 9 ); 10 11 return rml` 12 <div onclick="${source(buttonClick, datasetValue, total)}"> 13 14 <button data-value="1">add one</button> 15 <button data-value="2">add two</button> 16 <button data-value="3">add three</button> 17 18 </div> 19 20 Sum: <span>${total}</span> 21 `; 22};
As you can see, the main data model, which is the observable stream called total
, receives number
and emits number
.
The DatasetValue
Event Adapter translates raw DOM events into the plain numbers required by the model.
Finally, we're leveraging the DOM's standard Event Delegation by only adding one listener to the container, rather than to each button. We're making sure only button clicks are captured by using the ButtonClick
filter
The main difference between Event Adapters and Event Adapter Operators is that the former are designed for simplicity and convenience. Their name begins with an uppercase letter.
Adapter Operators, on the other hand, are simple, plain-old RxJS operators. Their name is camelcased and can be composed together just like other RxJS operators.
Rimmel often exports both. For instance, the Cut
Adapter, or the cut
Operator clear the value of a text input before emitting its value. They are convenient ways to let users repeatedly enter information and clean up a text field for the next input.
The following example illustrates the use of the Cut
Event Adapter to feed a simple text stream.
1import { rml, Cut } from 'rimmel'; 2import { Subject } from 'rxjs'; 3 4const Component = () => { 5 const stream1 = new Subject<string>(); 6 7 return rml` 8 <input onchange="${Cut(stream1)}"> <br> 9 10 Input text: <span>${stream1}</span> 11 `; 12};
The following example, will instead use the cut
Operator, combined with an onEnterKey
filter and a toUpperCase
operator to only emit and clean up the field's value when the user presses Enter
.
1import { map, filter } from 'rxjs'; 2import { rml, source, cut } from 'rimmel'; 3 4const Component = () => { 5 const onEnterKey = filter((e: KeyboardEvent) => e.key == 'Enter'); 6 const toUpperCase = map(s: string) => s.toUpperCase(); 7 8 const stream2 = new Subject<string>(); 9 10 return rml` 11 <input onchange="${source(onEnterKey, cut, toUpperCase, stream2)}"> <br> 12 13 Uppercased input text entered: <span>${stream2}</span> 14 `; 15};
Please note how the last parameter of source()
is stream2
the actual target of the input pipeline.
Rimmel supports two categories of sinks: standard (specialised) sinks and dynamic sinks.
Standard sinks are the simplest and most intuitive ones: those you define in an everyday template from which the type of data binding required can be easily inferred. They are optimised for performance, updating a specific part of the DOM.
These include:
Some sinks are implicit as the surrounding HTML and/or CSS context makes it clear what they should do. E.g.:
1<!-- set class names --> 2<div class="${stream}"> 3<!-- set inline style --> 4<div style="${stream}"> 5<!-- set background colour --> 6<div style="background-color: ${stream};">
Other sinks can be specified explicitly when it's not obvious from the context or when several options are available to display the same content:
1<!-- Remove dangerous scripts from the stream before rendering --> 2<div>${Sanitize(stream)}</div>
1<!-- Sink the stream's content into `.innerText`, rahter than `.innerHTML` (which is the default) on an element --> 2<div>${InnerText(stream)}</div>
1<!-- You take control and decide exactly how that stream should be rendered --> 2<!-- Useful for complex data structures --> 3<div>${YourCustomSink(stream)}</div>
1// InnerHTML (this is implicit, from the context) 2const stream = new Subject<HTMLString>(); 3target.innerHTML = rml`<div>${stream}</div>`; 4 5// InnerText (the easy way to prevent XSS) 6const stream = new Subject<HTMLString>(); 7target.innerHTML = rml`<div>${InnerText(stream)}</div>`; 8 9// Class 10const stream = new Subject<CSSClassObject>(); 11target.innerHTML = rml`<div class="${stream}"></div>`; 12 13// Style 14const stream = new Subject<CSSStyleObject>(); 15target.innerHTML = rml`<div style="${stream}"></div>`; 16 17// CSS 18const stream = new Subject<string>(); 19target.innerHTML = rml`<div style="color: ${stream};"></div>`; 20 21// Data Attribute 22const stream = new Subject<string>(); 23target.innerHTML = rml`<div data-attribute="${stream}"></div>`; 24 25// Disabled Attribute 26const stream = new Subject<boolean>(); 27target.innerHTML = rml`<button disabled="${stream}">clickable</button>`; 28 29// Removed (this is a RML attribute, not a standard HTML one) 30const stream = new Subject<boolean>(); 31target.innerHTML = rml`<div rml:removed="${stream}"></div>`; 32 33// Closed 34const stream = new Subject<boolean>(); 35target.innerHTML = rml`<dialog closed="${stream}">...</dialog>`; 36 37// Generic Attribute 38const stream = new Subject<string>(); 39target.innerHTML = rml`<div some-attribute="${stream}"></div>`; 40 41// "Sanitize" - display render-safe HTML 42import { rml, Sanitize } from 'rimmel'; 43 44const stream = new Subject<HTMLString>(); 45target.innerHTML = rml`<div>${Sanitize(stream)}</div>`; 46 47// A "Custom" Sink to just make output uppercase 48const UpperCase = sink( 49 map(strHTML => strHTML.toUpperCase()) 50); 51 52const stream = new Subject<HTMLString>(); 53target.innerHTML = rml`<div>${UpperCase(stream)}</div>`;
Dynamic sinks, on the other hand, are optimised for size and designed for convenience. They can take anyhing as input and figure out how to update the DOM at runtime.
Mixins are an exciting by-product of dynamic sinks, which allow you to inject pretty much anything at any time (event listeners, classes, attributes, etc) into the target "host" element by means of emitting a DOM Object
— a plain-old object whose properties and methods represent DOM attributes and event listeners.
1const Mixin = () => { 2 const onmouseover = () => console.log('mouseover') 3 4 const onclick = new Subject() 5 6 // Emit 'clickable' first, 7 // then 'clicked' afterwards 8 const classes = onclick.pipe( 9 mapTo('clicked-class'), 10 startWith('clickable'), 11 ); 12 13 // <! ----------------------------- 14 // The following DOM Object will be 15 // "merged" into the target element 16 // <! ----------------------------- 17 return { 18 onclick, 19 onmouseover, 20 class: classes, 21 'data-new-attribute': 'some value', 22 } 23} 24 25// ---------------------------- 26// And this is how you call it: 27// ---------------------------- 28const component = () => { 29 return rml` 30 <div ...${Mixin()}></div> 31 `; 32}
When the above component is rendered on the page, the mixin will inject everything else into it, including the onclick
and the onmouseover
event handlers, a statically defined data-new-attribute
and a "merge-in" observable stream, classes
to set classes dynamically.
Whenever the classes
stream emits, you will be able to set/unset class names in the component.
You may have noticed that mixins doesn't get any reference to the elements you merge them into.
In the imperative paradigm you would indeed have one, so you can imperatively set hostElement.classList.add('big')
.
In order to stick with good functional principles, mixins only return this DOM Object
, which is a declarative way to tell Rimmel what you want it to set on the host element.
1 const returnValue = <DOMObject>{ 2 onclick, 3 onmouseover, 4 'class': classes, 5 'data-new-attribute': 'some value', 6 'rml:closed': closedStream, 7 'disabled': disabledStream, 8 'readonly': readonlyStream, 9 'innerHTML': contentStream1, 10 'appendHTML': contentStream2, 11 // etc... 12 }
A simple use case for mixins is drag'n'drop, in the form of an isolated, reusable piece of functionality.
The code above is a simple function that performs its business and returns a DOM object.
Whatever it contains is merged into the host element.
Static key-values (e.g.: class
) are merged on the spot, before mounting.
Promises and Observables get merged whenever they resolve/emit.
Performance is always key, isn't it?
Actually, it depends. Some studies, in fact, show that a little bit of waiting in certain conditions can actually improve the overall user experience! (Bryant&Veroff, Kahneman&Tversky, Brickman&Campbell, Schultz)
Anyway, Rimmel is fast. You can slow it down with the Rx.delay()
operator if you want, but if you don't, it's fast and here's why:
It doesn't use a Virtual DOM. If you ever believed the story that the DOM is slow and re-running your components' code every time you blink an eye is the fast and good thing to do, you may have been the victim of a scam.
Rimmel updates the DOM using "precharged" Sinks, which are just some little element-bound functions that won't need to perform any lookup, but are just ready to update the DOM as fast as possible.
As a result, your updates in certain cases may happen even faster than the normal document.getElementById(target).style.color = newColor
.
We call this the Vanilla+ Speed.
Lightweight bundle size. V1 was just 2.5KB of code. Now it's a little more as we're sorting out a few things with specialised sinks and we are in a bit of a feature rush, but the aim is to fall back below 1KB with the launch of the new template compiler.
The rest is on you. Rimmel is a minimalistic UI library, but the true reason for its immense power is RxJS being behind its reactivity.
Do you have a Combobox with 1M rows?
A large spreadsheet with 1k x 10k reactive cells?
An HFT stock ticker with 10000 subscriptions?
These are obviously cases where "fast updates" or raw speed are irrelevant and what truly matters is adequate frontend architecture.
These are perfect cases to create Custom Sinks implementing relevant design patterns to make extreme scenarios still work optimally in an ergonomic, functional-reactive style that's easy to test and keep well organised.
If you come from some other libraries or frameworks, including "vanilla RxJS", you know you're somewhat responsible of cleaning up memory. The indiscriminate use of Observable subscriptions without proper cleanup can cause memory leaks in certain scenarios.
Using Observables with Rimmel is trivial. All DOM subscriptions and event listeners are handled by the library behind the scenes, registered when a component is mounted and unregistered when it's removed.
Do you have async data like an API call and a placeholder to display whilst waiting? We have good news: a BehaviorSubject
is all you need.
A BehaviorSubject
receives a special treatment from Rimmel in that its initial value will be rendered immediately, synchronously, whilst subsequent emissions will replace it as normal. This helps avoid unnecessary reflows and jank on the page.
1import { ajax } from 'rxjs/ajax'; 2 3const WaitingComponent = () => { 4 const stream = new BehaviorSubject('loading...'); 5 6 ajax.getJSON('https://api.example.com/data') 7 .pipe(map(data => `<pre>${JSON.stringify(data, null, 2)}</pre>`)) 8 .subscribe(stream) 9 ; 10 11 return rml` 12 <div>${stream}</div> 13 `; 14}
Suspense on Stackblitz.
Never forget to tag your templates with rml
, otherwise they won't be reactive.
This is a standard piece of HTML string:
1const str = `<div>hello</div>`
And this is a reactive template managed by Rimmel:
1const str = rml`<div>hello</div>`
It might sound unusual, but Rimmel can actually coexist with other frameworks. Your Rimmel component can be embedded in a React component and have children made in Vue, or even jQuery plugins or sit inside a larger jQuery application, or any other way around.
If you are planning a progressive framework migration, you can do it one component at a time.
We have created a few experimental AI assistants like RimmelGPT.js, to help you convert existing components, create new ones or just get started and have fun.
(Please note these are still highly experimental and various forms of severe hallucination can happen under different circumstances — YMMV)
Want to delve deeper into more advanced scenarios? Have a look at the following todo app all based on RxJS streams, ObservableTypes and Rimmel.
Here's a version of Tic Tac Toe made with Observable Streams
In a component like a colour picker there's a lot of derived state to manage, from various inputs and controls, mouse and touch actions, copy-paste, drag'n'drop. RxJS can handle these without blinking an eye whilst Rimmel just glues them together.
Remember, this is complex by nature and without RxJS it would be much worse!
Rimmel is a young project built with meticulous attention to detail and a bold vision: building a web ecosystem that "just works", using the functional-reactive paradigm.
Share the passion? Would like to help? That'd be hugely appreciated!
Come say Hi, there's a lot of work to do. The Roadmap is below.
To work with Rimmel locally, check it out then either with bun or other runtimes:
1npm install 2npm run test 3npm run build
There is a "kitchen sink" app you can use to play around locally, which should showcase most of what you can do with Rimmel:
1npm run kitchen-sink 2vite
Or you can just run it with one click on StackBlitz
There are discussions going on around making HTML and/or the DOM natively support Observables at WHATWG DOM/544 and especially the more recent Observable DOM.
Rimmel is closely following these initiatives and aims to align with them as they develop.
No vulnerabilities found.
No security vulnerabilities found.