In modern frontend work we often wrestle with leaking DOM details into our application logic: raw Event objects showing up where models should only care about meaningful data. Rimmel.js is one of the UI libraries around that offers a neat solution: Event Adapters. They let you transform DOM events into exactly what your data model needs. The result: clearer separation of concerns, easier testing, and code that’s simpler to reason about.
What Are Event Adapters?
Event Adapters are wrappers around DOM event handlers or RxJS streams that intercept raw events (e.g. MouseEvent, KeyboardEvent, etc.), extract or transform the parts that are relevant, then forward them into your model or stream in a clean format. They help you avoid this pattern:
// A model that works on a number, but gets a DOM event
button.onclick = (e) => {
const num = Number(e.target.dataset.value)
myStream.next(num)
}
With Event Adapters, you declare the transformation in the view binding so that your model always receives a properly typed value (e.g. number, string, custom object), without ever dealing with raw Events.
Rimmel provides a number of built-in Adapters, plus tools to compose your own.
Built-in Adapters & Operators
Here are some of the built-ins in Rimmel:
Value
, ValueAsNumber
, ValueAsDate
, etc.: adapters that extract the value from an input field (as a string, number, date).
Cut
: like Value, but clears the input after reading it.
OffsetXY
: for mouse / pointer events, converting the event into [x, y] coordinates.
Rimmel supplies a function called inputPipe to build custom adapters using these operators.
How Event Adapters Fit Into Rimmel’s Flow
In Rimmel:
Event Sources are things like DOM events, observable wrappers, etc.
Event Adapters sit between those sources and your target models or streams. They transform the raw event.
Sinks are where data ends up (e.g. DOM updates) via bindings in your rml
templates.
So the chain is something like:
DOM event
→ Event Adapter
→ Model stream / Subject
→ Sink
→ DOM update
By moving transformation logic into the adapters (often in the view/template binding), your model stays agnostic of how data arrived. It only sees “clean” input.
Why They Matter
Here are several reasons to adopt Event Adapters:
Cleaner Templates & Model Code
Templates stay declarative and free from business logic. Models/streams deal only with clean data.
Type Safety & Predictability
If your models expect certain types (numbers, strings, arrays, custom objects), adapters ensure that what goes in matches those expectations.
Easier Testing
You can test your models by feeding them simple values, not full DOM Event objects. That simplifies mocks & test setup.
Reusability
The same adapter can be applied in many places. You avoid rewriting the same map(value => ...)
etc.
Decoupling view implementation from model implementation
Your model doesn’t need to know if data came from onchange
, oninput
, onclick
, etc. It just receives the processed payload.
Examples
Here are a few examples showing Event Adapters in action.
Simple Input: Extracting Value
Without event adapters:
const stream = new Subject<Event>().pipe(
map(e => e.target.value)
);
target.innerHTML = rml`
<input onchange="${stream}">
<div>${stream}</div>
`;
With an event adapter:
import { Value } from 'rimmel'
const stream = new Subject<string>()
target.innerHTML = rml`
<input onchange="${Value(stream)}">
<div>${stream}</div>
`;
Clearing Input After Reading (“Cut” Adapter)
import { Cut } from 'rimmel'
const stream = new Subject<string>()
target.innerHTML = rml`
<input onchange="${Cut(stream)}">
Submitted: <span>${stream}</span>
`;
Here, after the user changes the input, Cut
emits the value and resets the input. The model just gets the string.
Composite Transformations: Filtering + Mapping
You can build your own event adapters with inputPipe
:
import { inputPipe, value } from 'rimmel';
import { filter, map } from 'rxjs'
const UpperOnEnter = inputPipe(
filter((e: KeyboardEvent) => e.key === 'Enter'),
map((e: KeyboardEvent) => e.target.value.toUpperCase())
);
const state = new Subject<string>()
target.innerHTML = rml`
<input onkeydown="${UpperOnEnter(state)}">
<div>${state}</div>
`;
In this setup, only when the Enter key is pressed does your model get the uppercase value of the input. All DOM-event stuff is handled by the adapter.
How to Create Your Own Adapter
If the built-ins don’t cover your case, making a custom Event Adapter is straightforward:
Use inputPipe(...)
to build a pipeline of RxJS operators that transforms the Event.
Ensure your pipeline ends in sending data into an observer/subject that corresponds to your model.
Export it as an uppercase function so it’s easy to use in your template. By convention, event adapters are uppercase.
For example, adapters for dropping files:
import { rml, inputPipe } from 'rimmel';
import { map, filter } from 'rxjs';
const FilesOnly = inputPipe(
map((e: DragEvent) => (e.dataTransfer?.files || [])),
filter((files: FileList | File[]) => files.length > 0)
);
// Usage inside a component/template
const fileHandler = new Subject<File[]>()
document.body.innerHTML = rml`
<div ondrop="${FilesOnly(fileHandler)}">
Drop files here
</div>
`;
;
Considerations & Best Practices
Don’t over-transform in event adapters; keep them focused. If logic gets too complex, it may belong in the model rather than in adapters.
Consider performance: lots of filters, maps etc. are fine, but chaining many might have overhead.
Mind keeping side-effects out of adapters; they should transform, not cause external changes.
Use adapters to enforce invariants (e.g. non-empty strings, valid numbers) so downstream logic is simpler.
Conclusion
Event Adapters in Rimmel.js are a small but powerful and elegant tool. They help keep your UI code clean, your model free of DOM plumbing, and your app easier to test and maintain. When building non-toy applications, that separation pays off more than you might expect.
If you build with Rimmel.js, one of the first things you should adopt is a consistent strategy for Event Adapters: define common adapters for your app’s patterns, and use them everywhere. You’ll find your layers of code become more composable, predictable, and ultimately more pleasant to work with.
Top comments (0)