Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

render() should not use a <span> to contain rendered elements #151

Closed
marad opened this issue May 30, 2020 · 20 comments
Closed

render() should not use a <span> to contain rendered elements #151

marad opened this issue May 30, 2020 · 20 comments

Comments

@marad
Copy link
Member

marad commented May 30, 2020

The render() function internally uses span as it's container element. Removing it would be a god send, but I don't think there is an easy way to do this so what we can do instead is provide a way for user to give it some custom element as container.

The problem I've stumbled upon goes like this. I want to create dynamically filled dropdown using fomantic. So I did this:

val areaId = KVar("")
div(fomantic.ui.search.selection.dropdown).new {
  input(type = InputType.hidden).setValue(areaId, updateOn = "change")
  i(fomantic.dropdown.icon)
  div(fomantic.menu).new {
    render(state.listAreas()) { areas ->
      areas.forEach { area -> div(fomatnic.item).setAttributeRaw("data-value", area.id).text(area.name)
    }
  }
}

This works, but because of the span it doesn't look right (styling is wrong). So instead I think we could do this:

val areaId = KVar("")
div(fomantic.ui.search.selection.dropdown).new {
  input(type = InputType.hidden).setValue(areaId, updateOn = "change")
  i(fomantic.dropdown.icon)
  render(stat.listAreas(), container = div(fomantic.menu)) { areas ->
    areas.forEach { area -> div(fomatnic.item).setAttributeRaw("data-value", area.id).text(area.name)
  }
}

Tell me what do you think.

@marad
Copy link
Member Author

marad commented May 30, 2020

I just thought that if we went this way it would be pretty easy to make an extension function that would make things even cleaner (IMO anyway 😄)

div(fomantic.menu).render(state.listAreas()) {
  // ... 
}

I'm not sure about the name I think it would be confusing to call it render here. Maybe renderChildren or renderContents would be a better options.

Or go with something like dynamicNew 😃 which is obviously a joke, but with this API it's kind of apparent that new makes static content and this render* makes dynamic content for the block.

Thinking about it I'd even say that I'd move render function into the Element so we have an API like this, and then make the old extension function just do span().renderContents(kvar, block)

@marad
Copy link
Member Author

marad commented May 30, 2020

I've experimented with this a bit and it seem to work fine but I'm not sure what to do with this:

// this.onCleanup(false) {
// containerSpan.deleteIfExists()
// }
// this.onCleanup(true) {
// previousElementCreator.getAndSet(null)?.cleanup()
// kval.removeListener(listenerHandle)
// }

I think that the first onCleanup is not necessary because render is not managing the container anymore (it should probably just stay in the old render function), but the second one looks important and I have no idea how to achieve that inside the Element.

@sanity
Copy link
Member

sanity commented May 31, 2020

Requiring a wrapper element like the <span> for render {} was kinda a kludge from the start, to make it easy to replace the rendered content when the KVar updates, although I didn't realize it could impact appearance.

But ElementCreators remember the elements they create (in ElementCreator.elementsCreated) so it should be possible to skip the span wrapper provided the ElementCreator ensures that the right DOM elements are replaced and the replacements go in the appropriate positions under their parent element.

In exchange for doing this server-side housekeeping we shouldn't need any wrapper element at all - would that address your use case?

@marad
Copy link
Member Author

marad commented May 31, 2020

Yes, getting rid of wrapping <span> would do fix things for me. So as I understand - the render function on re-render should remove all elementsCreated and then start a rendering new ones? Does removed elements also get removed from elementsCreated?

@sanity
Copy link
Member

sanity commented Jun 1, 2020

I think if we reuse the same ElementCreator then it should be reset to the state it would have been in with no elements before we add any new elements, so yes I think we should remove elements from elementsCreated.

We'd also need to make sure that new elements get inserted at the same positions under the parent element as the replaced elements.

Another approach would be to use a new ElementCreator every time the KVar is re-rendered (deleting the old one and the elements that belong to it), but it's not clear to me at this point which would be cleaner.

@marad
Copy link
Member Author

marad commented Jun 1, 2020

Are there any other important factors? Like speed for example?

@sanity
Copy link
Member

sanity commented Jun 1, 2020

It should be possible to make this improvement without sacrificing speed. Currently we can just call Element.removeChildren() on the wrapper <span>, which lets the browser do the work of figuring out which elements to delete.

The proposed new approach would require the server to pass a list of the element IDs to be deleted to the browser which would then delete them, this should be a single websocket message. I don't anticipate this being inefficient unless there are a very large number elements to be deleted.

Oh, and we also need to ensure that any subsequently added elements are added in the same position as those they replace.

@marad
Copy link
Member Author

marad commented Jun 2, 2020

Oh, and we also need to ensure that any subsequently added elements are added in the same position as those they replace.

This one can be hard. If we don't have the container element the children added by render could be interleaved with other components. Then re-render would have a really hard time to address that.

@sanity
Copy link
Member

sanity commented Jun 2, 2020

Yeah, getting element replacement right in situations where there are sibling elements that are not part of the render {} block is the headache that prompted the <span> solution in the first place.

I'm confident it's doable though if we think about the problem the right way, needs more thought.

@sanity sanity changed the title Make render() function optionally take container element render() should not use a <span> to contain rendered elements Jun 21, 2020
@sanity sanity self-assigned this Jun 21, 2020
@sanity
Copy link
Member

sanity commented Jun 26, 2020

Note: version 0.7.19 doesn't yet eliminate the need for a container element, but it does allow you to change the type of element used with render(containerElementTag = "li") { .. }.

@marad
Copy link
Member Author

marad commented Jun 26, 2020

This is nice!
Would it work with classes like render(containerElementTag = "li.item")?

@sanity
Copy link
Member

sanity commented Jun 26, 2020

Not right now I'm afraid, I could add another parameter to render to support configuration of the container element - although this would just be a stop-gap until we can remove the requirement for the container element altogether.

The stop-gap would be something like:

render(containerElementTag = "li", containerConfig = { it.classes("item") }) { ... }

Not very elegant, but hopefully temporary - would this work for you?

@marad
Copy link
Member Author

marad commented Jun 28, 2020

Well it would work for this exact use-case. Why you don't like the idea of just passing Element as the parameter? User could preconfigure it however they like.

@sanity
Copy link
Member

sanity commented Jun 28, 2020

Good point, just released 0.7.20 which supports this:

render(container = { li().classes("item") }, value = v) { ... }

(The reason I'm not allowing a raw Element to be passed in his to ensure that the element is a child of the parent ElementCreator)

@marad
Copy link
Member Author

marad commented Jun 29, 2020

So would something like this work?

val el = li().classes("item")
render(container = { el }, value = v) { ... }

@sanity
Copy link
Member

sanity commented Jun 30, 2020

Yes - that would probably work with the current implementation - so I guess it was inaccurate to say it would ensure use of the parent ElementCreator, but it would encourage it (I view this as a stopgap measure until we can remove the need for a container element, it's far from perfect).

Is there a particular reason to pass in an Element rather than creating it within the container function?

@sanity
Copy link
Member

sanity commented Aug 20, 2020

Relevant for appending multiple elements:

Notes:

  • ElementCreator should be able to add elements to a DocumentFragment rather than just a parent element.
  • CreateElement instruction should be generalized to support this

@sanity
Copy link
Member

sanity commented May 18, 2021

Somewhat related: #128, #177

@sanity
Copy link
Member

sanity commented May 31, 2021

After quite a bit of thought and discussion with @Derek52, I think I have a solution.

While it would be nice if we could do this without needing to add any additional elements to the DOM - I don't think this will be practical because any approach I can think of for keeping track of which elements need to be replaced has some very nasty edge cases.

Rather than wrapping the rendered elements in a noop marker <span>, we add a noop "begin" before the rendered elements, and an "end" after them. Note these spans will be siblings of the newly added elements. The ids of these marker spans are noted within the render function.

We modify ElementCreator so that instead of providing a position, we can provide the id of an element before which any new elements will be added using insertBefore(). We can use this to insert elements before the end - which will ensure they're added in the correct order.

We should also predefine a JavaScript function that can be given two element ids and which will delete any sibling elements between them. This can be used by render to clear previously added elements before re-rendering. It will be important to ensure that ElementCreator.cleanup() is called for the deleted elements.

@sanity sanity assigned Derek52 and unassigned sanity Jun 27, 2021
@sanity sanity assigned sanity and unassigned Derek52 Aug 21, 2021
@sanity
Copy link
Member

sanity commented Oct 9, 2022

Render was redesigned a while ago to remove the <span>.

@sanity sanity closed this as completed Oct 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants