Closed rymohr closed 8 years ago
Hi @rymohr thanks!
The idea behind {:list}
is that we need to support two scenarios:
wrap : Html -> Html
wrap item = <li><label>The item is:</label>{item}</li>
and
listOf : [Html] -> Html
listOf items = <ul><li>First hardcoded item</li>{:items}</ul>
In the fist scenario, the <label>
and item
would end up in the same list, as children of <li>
, namely:
wrap : Html -> Html
wrap item = Html.li
[]
[ Html.label [] [Html.text "The item is:"]
, item
]
while in the second scenario the hardcoded <li>
and {:items}
are two lists that get concatenated into <ul>
's children, that is:
listOf : [Html] -> Html
listOf items = Html.ul
[]
([Html.li [] [Html.text "First hardcoded item"]] ++ items)
You can check these two examples and play around with them in the live cheatsheet (they are "Nested Html" and "Nested list of Html").
Cheers!
Thanks for the examples. I guess my follow up question then is, when would you ever not want siblings concatenated if they both fall under the same parent?
And what is unique to elmx that prevents it from using an identical syntax to react's jsx?
Hi @rymohr these are both good questions.
when would you ever not want siblings concatenated if they both fall under the same parent?
You always want the siblings concatenated, but sometimes you want to concat a static list of siblings to a dynamic one:
numbers : Bool -> Html
numbers addExtra =
let
items = if addExtra
then[ <li>Two</li><li>Many</li> ]
else []
in
<ul><li>One</li>{:items}</ul>
and sometimes you just want to append a single element to the static list of siblings:
numbers : String -> Html
numbers more =
let
item = <li>{=more}</li>
in
<ul><li>One</li>{item}</ul>
which brings us to the second question:
And what is unique to elmx that prevents it from using an identical syntax to react's jsx?
In other words, why cannot {:items}
above be just {item}
?
The answer is twofold:
React.createElement
exploits JS dynamic types to allow both single elements and lists of elements to be passed as children, thus enabling the {x}
syntax to represent both.Lets look at React in more detail!
When you write this:
const list = (items) => <ul><li>First</li>{items}</ul>
The resulting JS looks like this:
var list = function (items) {
return React.createElement(
'ul',
null,
React.createElement(
'li',
null,
'First'
),
items
);
};
The first argument to React.createElement
is the tag name, the second the attributes, and from that point on, the rest are either elements (like 'First') or lists (like items
). If we had to implement this createElement
function ourselves, we could probably go for something like:
function ourCreateElement(tag, attrs, ...children) {
// normalizedChildren :: [ [Element] ]
const normalizedChildren = children.map(
c => (isArray(c) ? c : [c])
);
// do something with normalizedChildren, knowing is always a list of lists of
// elements (e.g. flatten and render)
}
const isArray = o => o.length !== undefined // or something more fancy
In Elm, using elm-html
similar code would look like:
list : List Html -> Html
list items =
Html.ul [] ([ Html.li [] [ Html.text "First" ] ] ++ items)
Note the type of Html.ul
:
ul : List Attribute -> List Html -> Html
The second argument must be a list of Html
, cannot be a single Html
nor a list of mixed stuff (Html
s and lists of Html
).
So lets think about this in a different way...
Given the following piece of JS:
const Comp = (i) => <div>{i}</div>
can you tell during compilation time (i.e. when translating JSX into JS) whether i
is a single element or a list?
The answer is "no", because there could be two instances of the Comp
component one passing multiple elements in i
and another passing a single one:
const A = () => {
const i = <span>Hi</span>;
return <Comp i={i} />;
}
const B = () => {
const i = [1,2,3].map(v => <span key={v}>{v}</span>);
return <Comp i={i} />;
}
Since the code for Comp
is generated only once and must work for both scenarios above, it follows that Comp
works for both single elements and lists (exploiting React.createElement
's flexibility).
If React.createElement
was statically typed (as it would be if it was implemented in Elm), then the signature would probably be similar to that of Elm's ul
:
fakeCreateElement : String -> List Attribute -> List Html -> Html
fakeCreateElement tag attrs children =
...
And then the magic of JSX allowing both single elements and lists would not work.
So, long story short, in JSX everything works because JS is dynamic and a function can take a list one day an element the next day and a hobbit some time later.
In Elm you need to make things explicit (and that is a good thing) so we cannot have the magic {x}
work for anything (or, at least I cannot think of a way or making this work).
Isn't that exactly what union types are for?
Yes, unfortunately (for this particular use case, but luckily for everyone's mental sanity) Html
is not a union type:
type alias Html = VirtualDom.Node
(from Html.elm)
Nice work! Can you expand on why the : operator is required for lists? Seems like you would always interpolate if the list was a elmx fragment. Requiring = for string interpolation definitely makes sense.