Skip to content

Conversation

@TudorGR
Copy link
Contributor

@TudorGR TudorGR commented Oct 29, 2025

Summary

Fixes #64 the bug where a Promise resolving to false would incorrectly add a class name to an element.

The Problem

When using class="${{dotted}}" where dotted = Promise.resolve(false), the class was being added even though the promise resolved to false. This happened because ClassObjectSink was checking if the Promise object itself was truthy (it always is) instead of waiting for the resolved value.

The Solution

Modified ClassObjectSink to:

  1. Check if a value is a future (Promise or Observable) using isFuture()
  2. If it's a future, defer the add/remove decision until after it resolves
  3. Make the decision based on the resolved value instead of the Promise object

Changes

  • src/sinks/class-sink.ts: Added isFuture check and conditional logic
  • src/sinks/class-sink.test.ts: Added 3 test cases covering promises resolving to false/true

Testing

All tests pass ✅:

  • New test: Promise resolving to false does NOT add class
  • New test: Promise resolving to true DOES add class
  • New test: Direct false value does NOT add class (baseline)
  • All existing tests continue to pass (9/9 class-sink tests, 143/143 total)

Example

const dotted = Promise.resolve(false);
rml`<div class="${{dotted}}">should NOT be dotted</div>` // ✅ Now works correctly!

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rebase your code, please, as this stuff has already been merged from another PR?

expect(el.className).not.toContain('class3');
});

it('should not add class when value is false (promise resolved to false)', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, can we just give these a little bit of structure, please, so it's easy to see how these are organised, what they are covering and ultimately if there's anything that might be missing, or if new tests need adding, where exactly should those be put?

describe('when a property of the object is a present value', () => {
	...
});

describe('when a property of the object is a future value', () => {

	describe('when it resolves/emits true', () => {

		it('should add a class corresponding to the property name', () => {

	describe('when it resolves/emits false () => {

		it('should not add a class corresponding to the property name', () => {

// If v is a future (Promise or Observable), defer the decision
// until it resolves, otherwise check immediately
if (isFuture(v)) {
asap((resolvedValue: any) => resolvedValue ? add(k) : remove(k), v);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any chance we can use the subscribe function, here, instead? That should also flatten promises/observables down

@dariomannu
Copy link
Contributor

adding hacktoberfest-accepted label, as the fix is fine, even if we're just adding a few minor improvements and finessing, in case you're participating in Hacktoberfest...

@TudorGR TudorGR force-pushed the fix/issue-64-promise-false-class branch from 6c5316c to 6ba097f Compare October 31, 2025 14:13
@TudorGR TudorGR requested a review from dariomannu October 31, 2025 14:15
// Use asap to handle both present and future values
// For futures (Promise/Observable), it will wait for resolution
// For present values, it will execute immediately
asap((resolvedValue: any) => resolvedValue ? add(k) : remove(k), v);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was it not feasible to use subscribe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into using subscribe, but it requires a node parameter as the first argument:

export const subscribe = (node: Node, source: MaybeFuture<T>, next: ...) => { ... }

Since we're inside the forEach callback and don't have the node in scope at that point, asap seemed like the right fit. It's also the pattern used in other sinks like content-sink.ts, dataset-sink.ts, and style-sink.ts.

Should I refactor this differently?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, yeah, you're right... good point.
So, we're all good, then, the rest looks great, LGTM!

@dariomannu dariomannu merged commit 4f9b407 into ReactiveHTML:master Nov 3, 2025
@dariomannu
Copy link
Contributor

@TudorGR all good and working, great work!
If you like Rimmel.js, please consider dropping a star to let other people find it easier

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A "false promise" should not set a class name

2 participants