Open freewind opened 9 years ago
Thanks for your suggestion. It would significantly improve the readability.
As it requires some restructuring and making all widgets immutable, it is quite an undertaking. Therefore, we should implement it in v0.3, which will be the next major release.
Thanks!!
My approach would be to implement it as follows:
case object Widget extends Widget
class Widget {
private var css = ""
@inline def copy(f: Widget => Unit): Widget = {
val res = new Widget
res.css = css
f(res)
res
}
def css(s: String): Widget = {
copy { c =>
c.css = s
}
}
// Return rendered widget
def apply(views: View*) = ???
}
Can we come up with something simpler? This solution is not very convenient because we'd have to define copy()
manually, and the approach wouldn't work that well with inheritance. I'd also prefer if all properties could stay immutable.
Just tried a simple solution:
def view() = "#main-page" >>> div(
"#main-form.form.search" >>> div(
".search-panel" >>> div(
".search" >>> text().bind(keyword).placeholder("Search")
)
),
div()
)
How do you feel it?
The solution you're suggesting doesn't look too bad. Unfortunately, it's only a partial fix. For example, if we were to register event handlers, we'd still need to place them at the end of the widget initialisation.
I believe we should use a macro-based approach that initialises the object in one go (unlike my previous suggestion which makes many unnecessary copies).
Another change I'm considering is to separate the widget creation from the actual DOM rendering:
val recipe = div // immutable widget
.css("search-panel")
.id("search")(
text()
)
val rendered = recipe.render // rendered DOM node
val rendered2 = recipe.id("search2").render // modifying widgets does not have any side-effects
Due to their immutability, widgets would be compatible with the JVM. There could be interesting use cases such as UI testing or serialisation. We could instantiate widgets on the server and send the serialised objects to the client, where a simple .render
call would create the DOM nodes.
Also, the macro could be adapted to parse HTML templates from inline strings or external files:
val recipe =
html"""
<div class="search-panel">
<input type="text" bind="${keyword}" placeholder="Search" />
</div>
"""
val recipe2 = tpl("_search_panel.html")
I used my approach in a small project and basically satisfied so far (compared to the standard approach), because it's much easier to find the elements and write the styles in lesscss, and the implementation is quite easy.
The html version will give much better readability and I really like it. With the help of IDEA, I can mark a string as 'html language', so I can get the syntax highlight and completion.
A question that why you didn't mention the xml version with macro? Another project scala-js-react provide the xml version (although it's buggy)
Support for XML literals will soon be dropped in Scala. That's why I was suggesting to use string interpolators instead.
Cool, @tindzk !
Sorry, maybe it's better to keep it open :)
@tindzk , I think that the combination of the macro-based approach and separating the creation and rendering looks like a really good idea!
I don't know enough about scala macros to have a good opinion about a macro-based solution versus something else, but the syntax feels very natural and it would be very nice to avoid the copy-code in your first example. Also, the possibility to write html"..." snippets etc would be very useful!
Can you describe how/where the channel-bindings would take place? It must be done in the "render-phase" i assume? With html-templates this can be done with interpolation (bind-attribute?). How would it work when using "scala-recipes"? For example:
val name = Var("some name")
// old way
val myWidget = div(name.map("Name: " + _))
// how would you do this when creation and rendering is separated?
// just an example to explain what i'm after..
val myWidget = div("Name: $name").render.bind("name", name)
I just saw another possibility in a React Native SJS wrapper project: https://github.com/chandu0101/scalajs-react-native/blob/master/core/src/main/scala/chandu0101/scalajs/rn/components/Text.scala#L25
where the attributes are set in a first and then call apply() with the content:
View(style = Styles.container("red"))(
Text(style = Styles.text)("Welcome to Scala-JS ReactNative"),
Text(style = Styles.text)("To get started, edit HelloNative.scala "),
Text()
)
The attributes then should either have default values or be Option with default None.
The only drawback I can see is that if you don't want to specify any attributes, an empty () needs to be added:
Text()("Content")
Maybe an idea for widgets with required attributes or where it is useless without parameters, like an Anchor widget:
Anchor(url = "github.com")("Github")
What do you think about that? Advantages are that it is placed in front, and typesafe, and it is easy to discover the available attributes, and IDE support is good.
Well, another drawback of the solution described above is that it is harder to support both reactive and static parameters. One could make one with only reactive attributes and one with only static attributes, but a combination might be desired depending on the usage.
That's exactly the reason why I decided against this syntax. In earlier revisions of the DSL I was using the syntax as you described it, but then the need for reactive parameter updates arose.
On the other hand, it may actually be a good idea to only allow constant values in the specification because that allows us to have an immutable representation of the tree. Reactive parameters should only be performed on the instantiated/rendered tree. That's how it's done right now in MetaWeb and I believe that a DSL should respect a similar distinction.
Yes, it will be interesting to look at the MetaWeb ideas.
It could be supported in a nasty way by including both static and reactive parameters wrapped in Option[] with default value None, but then its hard to require parameters without inventing a new hierarchy.
Btw, the tag mappings looks a bit redundant when all are already mapped by sjs dom library.
Do you mean MetaWeb's bindings? They are only redundant if you think of your application in terms of Scala.js. However, not relying on JavaScript's DOM allows us to render a static version for the JVM and even do pre-population for search engines.
I mean the Widok bindings. Look at how e.g. this line in my new PR is just delegating: https://github.com/widok/widok/commit/c82bd08924d4cc643077544014c1ad2c9b35ddb6#diff-cf4957338261c0c2ad6021cc51230fa1R397
But server side rendering support would be nice. Compared to React and also the wrapper, Widok/MetaWeb would not need to execute javascript on the server, so they will have an advantage there.
For a simple html page with scalatags:
You can see the
id
andcss
are put after the tag, which is not easy to locate a tag by its id/class.What about provide a syntax which allow we put
id
/css
in the start? e.g.