rml() that return source streams #8
Replies: 3 comments 7 replies
-
Hi @ftaiolivista, Thanks for the nice words :) First off, let me add a few notes just to have some clarity on naming and avoid confusion. Now, Rimmel doesn't technically define or mandate what a component is or should look like. Whatever is in the docs comes from ergonomics-driven patterns emerged with use, so there is still a lot of freedom and flexibility with regards to future development. The typical component pattern that emerged over time twists (challenges) the unidirectional aspect above a little bit by allowing both sources and sinks to come as parameters of a component-function. I found this to help ergonomics and to make code less verbose, but feel free to show me wrong on this. It is just "syntactically" unidirectional in the sense that it's all top-down according to the syntax, both sources and sinks are parameters, but then we obviously know these are distinct "upstreams" and "downstreams": const Component = ({ downstream, upstream }) => rml`
<button onclick="${upstream}">${downstream}</button>
`; Now, if we want to compose components, we start with our models: // first we create a stream as a standalone "model" that
// can be reused anywhere and tested on its own:
const Counter = (initial) => new BehaviorSubject(initial).pipe(
scan(x=>x+1)
); Then we put everything together: const Parent = () => {
// you still create subjects, but you can "hide" the fact you do,
// how about that?
const c = Counter(0);
return rml`
<div>${c}</div>
<div>${Component({ content: c, onclick: c })}</div>
`;
}; This structure enables a component to always returns a template (an HTML string), this time in a way that's more similar to Solid and React. If I had to draw a line between the two, I'd say Cycle components are designed to look more like streams themselves and Rimmel components are more like "HTML Monads" ( Now, let me try to understand what you're proposing here, which is:
This is just a template, but a component could be in fact structured in many ways, as you say, by having an alternative to the const Unidirectional = (sink) => flowml`
<label>
<input type="button" value="1" onclick="@source1" />
${sink.HTML}
</label>
`; So, this new entry point would return something like const Wrapper = () => {
const { HTML, source1 } = Unidirectional(down);
return flowml`
<div>
${HTML}
<hr>
Data: <span>${source1}</span>
</div>
`;
};
document.body.innerHTML = Wrapper().HTML; Having this flowml function return all sinks makes it really convenient to have general-purpose components that can deal with not just HTML, but anything, really. const SuperComponent = () => {
const stream1 = ...;
const stream2 = ...;
const visuals = html`
<div>${stream1}</div>
`;
const localStorageStuff = combine(stream2, whatever);
return { HTML: visuals, localStorage: localStorageStream };
}; This would be really, really great, as one of these supercomponents could also be used to sink LocalStorage, WebSockets, etc. I've been thinking about this problem for a long time, actually, and still exploring various variants and alternatives. Now, apart from this, an other important observation comes up, too: if you look carefully, in either case, both in Rimmel and Cycle, at some point you do have to create some bidirectional streams. Typically in Cycle you do it in the parent, from the child: const Parent = () => {
const input = whatever();
const { output, effects } = Child(input)
return someTemplateOf(output);
}; In Rimmel the same is rather left with the child component: const Parent = () => {
const input = whatever();
return someTemplateOf( Child(input) );
}; What I would argue is that in both cases you're doing the same thing, just in the parent vs the child. The key takeaway is maybe unidirectionality just doesn't exist and we're forcing it somehow onto components, where in fact we may be better off embracing bidirectionality instead, for components? The way it works now is that streams are typically unidirectional, components are bidirectional. With regards to what you're saying about having to create Subjects every time, which I understand, the options appear to be:
The only problem with the third option is that you'd lose syntax consistency: So, by embracing bidirectionality, even if it may feel weird at first (after some time I promise it can really feel natural), we can preserve the syntax validity without any extra tooling. We can refactor/rename as normal without fear of breaking the code. Bidirectional components were in fact a new discovery for me, too. It appeared to work, so I kept building on top of it and found they can very elegantly revive old, forgotten patterns such as the Event Adapter to help a better separation between models and views. Nonetheless, I appreciate that the idea of the "Supercomponent" comes with many important benefits, so I'm happy to get involved in further research around your proposal. What do you think could be a good use case for a few components connected together using Cycle's unidirectional flow? We could compare that for a start and see what comes out. |
Beta Was this translation helpful? Give feedback.
-
Thanks @dariomannu for the great answer, very interesting, you perfectly understood what I wanted to say. I think I'll take some time in the next few days to do some use case experiments and maybe I'll share them with you in this thread. In the meantime I've been thinking about the tooling problem. Could it be a solution to instantiate the sources directly in the template? It doesn't have any advantages compared to the current state of (rimmel, react, solid, etc...), in fact it has a more verbose syntax, but at least conceptually it seems more linear to me. And the Typescript correctly assign types out the returned streams. class Subject {}
class BehaviourSubject {}
function html<const T extends readonly [string, any][]>(
strings: TemplateStringsArray,
...args: T
): [string, { [K in T[number] as K[0]]: K[1] }] {
return [
// fake html building
strings.join('.'),
//fake argument parsing
Object.fromEntries(args) as { [K in T[number] as K[0]]: K[1] },
]
}
const [htmlStr, sources] = html`<div
onclick="${['source1', new Subject()]}"
onclick="${['source2', new BehaviourSubject()]}"
/>`
console.log(htmlStr)
// (property) source1: Subject
console.log(sources.source1)
// (property) source2: BehaviourSubject
console.log(sources.source2)
// Property 'source3' does not exist on type '{ source1: Subject; source2: BehaviourSubject; }'. Did you mean 'source1'?
console.log(sources.source3) P.S. |
Beta Was this translation helpful? Give feedback.
-
Looking forward to see what you come up with! Shout if you need help. Stackblitz is also amazing for these experiments. | In your opinion, could Rimmel already be usable in production? It's obviously a young library with all that comes with it, but I'd say it can be used to power certain production scenarios, maybe some level of support could also help, but it shouldn't have any more bugs than the mainstream frameworks already have, so you trade innovation for a little bit of stability, as we might tweak the signature of a function or two, in case, but apart from that, it works and all you can do with RxJS (which is very mature and stable) you can do here with the same degree of confidence. Rimmel may seem a complicated thing, but all it does is just binding observable streams to the DOM, acting as a thin wrapping layer... If that sounds reassuring, happy to help out and support any production endeavours. | I see in the roadmap "Support for text nodes" but it seems to me that it is already included. | Is SVG already usable in your opinion? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
First of all, congratulations on the project — it looks great.
I've been using Cycle.js for years and have been searching for a more modern alternative for new projects. While Solid.js has some interesting ideas, I personally don't enjoy its imperative style.
Rimmel (that I just discovered) feels exactly like what I’ve been looking for. I did have one idea I wanted to share.
Wouldn’t it be useful to have an alternative version of the rml() template function — one where the "sources" are specified using explicit textual references, and where the function returns a map of the generated source streams?
The goal here is to avoid the need to manually instantiate Subjects and instead promote a more unidirectional data flow.
For example:
flowhtml
<input type="button" value="1" onclick="@source1" /> ${sink}
This function would return a tuple: [htmlElement, { source1: Observable<> }] — the usual HTML along with the observable source1.
What do you think about this approach?
Beta Was this translation helpful? Give feedback.
All reactions