Repository navigation
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
254e000 to
d1db425
Compare
commit: |
0cbbd95 to
ab3c6cf
Compare
|
how do you think we will have multiple "documents"? (that's the only reason DOM has an owning document) is the have you made the "implementation" a parameter purely for testing? it looks like its actually the base class but isn't just so we can pass a mock base in during tests. overall i'm not sure we should copy the exact shape of the DOM because some of it doesn't give us anything. i'm yet again disagreeing with yet another attempt at the same thing though, i'm aware. so do as you wish |
|
Did we not all agree that there should be at least some vague shape of a DOM so that our bindings behave? That components have the vague shape of working with vanillajs so we can compose them in a similar manner to the browser and also allow an opt-in browser compat mode? The only parts that are the same from previous attempts are the parts I had thought we agreed on. @43081j, what is your understanding of our agreed shape here? |
how vague? here lies the difference in vision i suppose. i'm just going to brain dump what i would do and you can agree or disagree between yourselves, whatever you and the rest of the team feel. Mostly pseudo-code ahead. const box = new BoxElement();
const input = new InputElement();
box.appendChild(input);
// then for strings, either a special-case or a text element:
box.appendChild("foo"); // special case it
box.appendChild(new TextElement("foo")); // or make it an element
// children exist
box.children; // [InputElement, TextElement]
// event dispatch
input.dispatchEvent(ev); // built-in EventTarget#dispatchEvent, doesn't bubble
input.dispatchEvent(bubblingEvent); // does a basic traversal upwards via `.parent` and dispatches on each
// event listeners
input.addEventListener(name, fn);
box.addEventListener(name, fn); // would work for the bubbling case, `ev.currentTarget` is the inputinternally there's no concept of a "node" or a "component", there's one type: an element (i think i'd call it a component in fact but either works): // maybe inherit some base Element that gives us lifecycle stuff. not sure if necessary yet
class InputElement extends Element {
constructor() {
// this is a global event, some root term/container logic emits keydowns against the currently focused
// element.
this.addEventListener('keydown', this.#onKeyDown);
}
#onKeyDown(ev) {
// either also implement a global `input` event that accumulates strings, or do it ourselves in here
this.value = newValueUserTypedIn;
// either explicitly trigger a re-render
this.requestUpdate();
// or do it by "magic" by each public property being a setter that calls req update internally
}
render() {
// yield some raw operations
// now yield your children:
yield* renderChildren(this.children); // still yields to a bunch of raw ops recursively
}
}somewhere you then have this root thing that takes an element and knows how to render it: render(input); // literally does ONE render of this input, NOT a render loopsome other thing composes this to create a render lifecycle, like maybe the base class i mentioned: class Element {
appendChild(child) {
this.children.push(child);
this.requestUpdate();
}
requestUpdate() {
// optimise it some day by debouncing/queueing renders so multiple prop changes at once only trigger one render
if (propsChanged || childrenChanged) {
render(this.render());
}
}
}note that, at this point, this'd mean each child tries to also note there's no "render loop" here at the minute. so we'd still need some way of "stopping". i.e. things can just keep calling so yeah, in terms of "DOM like", we're just adopting the naming of the bits we already need, we're not trying to imitate the entire DOM. in these examples:
|
How do you account for the text node equivalent in the browser? We also need some stable identity for the frontend frameworks to reconcile against; how do you do that with the concept of a "node"?
Can you define what your mental model is for a "render loop" and what you are trying to avoid with it? From the sounds of it, you think we have some thing that owns the render api. I presume it would take repeated requests to render but counting as one to handle:
This PR doesn't wire up any events yet so we can consider that in the future. These child manipulation items we all are aligned on being a requirement. |
90890a2 to
07d4868
Compare
The only reason that Nodes have a reference to the
The body exists for familiarity and nothing more. It is just an alias for
As it exists, the
It could definitely be organized differently.
I must admit that this is not the shape that I had imagined at first either to be quite honest, but based on our discussion with the group on Wednesday, I was under the impression that we were shooting for a "less is more" approach and that since the dom is the lingua franca of all things JavaScript UI, we would just do as the DOM does as a default. Much like an assembly language: Nobody uses it, but everybody has a higher level construct that targets it. Same thing with Having said that, I am starting to think there might be a lot of potential with this approach, and that it is worth exploring. I implemented a rudimentary style system and it feels already like the styling is going to be very flexible. It's not much code, but it's already at the point where not much more is required to land React, Vue, Solid and friends on top of it. |
natemoo-re
left a comment
There was a problem hiding this comment.
overall i'm not sure we should copy the exact shape of the DOM because some of it doesn't give us anything. i'm yet again disagreeing with yet another attempt at the same thing though, i'm aware. so do as you wish
I must admit that this is not the shape that I had imagined at first either to be quite honest, but based on our discussion with the group on Wednesday, I was under the impression that we were shooting for a "less is more" approach and that since the dom is the lingua franca of all things JavaScript UI, we would just do as the DOM does as a default. Much like an assembly language: Nobody uses it, but everybody has a higher level construct that targets it.
I'm happy to own this as a pretty strong directional push from me. My opinion is that the DOM is notably wonky, but everything already targets it. There's a world where we implement only a modern subset of DOM concepts, similar to https://github.com/WebReflection/linkedom, or even make it possible for framework integrations to add their own Node behavior when needed for compatibility. If we keep to the contract we'll have many fewer surprises than trying to backport the DOM environment frameworks expect from our own conceptual model.
Excited to get this rolling! I know it won't be perfect, but let's figure out how to optimize once we have stuff running and can feel the rough edges.
|
@natemoo-re Sounds good. I'm also not sold on @43081j On the brain dump of pseudo specifically, I actually don't think we're far off directionally from what you sketched out.
Differences:
Not exactly, but very close. |
0bb0996 to
ec40edd
Compare
why do you need an equivalent of the browser's text node? on your second point, if JSX adapters or similar need an object representation of text, make a
i'm not trying to avoid anything, i'm just trying to describe how the render lifecycle would work. in a game, for example, you have a render loop: just loop forever outputting frames constantly. that doesn't need to be a concept here and isn't in browsers. here, we can render reactively. i was just comparing to known concepts. render, then when some state changes, render again, etc. the bit im unsure of is how you stop that. i suppose in an imaginary TUI if we had a but if you struggle with that model, do a loop. its fine
i would.
im not asking or caring too much about what the "implementation" is. i'm questioning where you've positioned it. i think its meant to be a base class and its only been made a property/parameter here because your head's in DI land or because you want to be able to pass a mock "implementation" in within tests. either way, i'd make it a base class. overall i don't believe we should be imitating the DOM to this extent. i think we should try name our similar behaviours the same way, but we shouldn't introduce noise just to make it look similar. so:
i would define the ones we need and name those similar or the same as DOM. |
i still wouldn't bother. we don't need it. that can be the job of the "adapters" we build.
i think this can be a lot simpler the way i described since then you can do what you wish with those. pass it to tty, assert it in tests, whatever. no separate abstraction. |
|
I think this is a key architectural difference of the dom-ish way. There is no point at which the tree is not fully rendered. The advantage is that Instead of polymorphic layout which can be impacted by internal private state, it must be expressed via a fully rendered and externalized style. Transcription into tty ops then becomes rote and easy to aggressively cache and transform with stylesheets. |
|
I'm strongly in favor of merging this and keeping the work moving—we'll have a better sense of rendering ergonomics once we get a feel for what works or doesn't. I don't think anything landing from this PR means the API is set in stone; we are very much in the rough sketch phase and this is about as stripped down as it could possibly be. +1 to merging |
FWIW, this second model is how the current prototype works. I.e. the loop is idle when nothing is happening. Each state change requests a turn of the render loop, with changes in the same NodeJS tick all coalesced into the same turn. Furthermore, changes requested while the loop is turning are also coalesced into the next turn. for await (const _ of this.#frames) {
const ops = [...this.#layout.layout()];
const result = this.#term.render(ops);
await write(this.#output, result.output);
}Among other things, this exerts a natural back pressure on the render loop so that it will not produce frames faster than they can be flushed to
In this particular setup, the frame generator listens to an abort signal and emits |
|
Also, based on feedback, I've made the following simplifications:
|
What does this PR do?
Reboot the concept of a host environment from the very outside in by adding the minimal machinery to render using a very dom shaped api. We start with the minimum:
And ensure that it renders to the screen using https://termless.dev to write the tests.
Layout
There is no render function proper, only that mutations to the dom are batched up into a single render frame, and then the entire tree is walked synchronously (caching can come in later) and every element is converted into operations which are then written out to TTY. My thinking is that we can use stylesheets to compute the actual properties of the box.
Type of change
Checklist
pnpm test)pnpm format)AI-generated code disclosure