Open jamesarosen opened 6 years ago
A related feature is the idea of capture. In Ember Handlebars, the above template might more naturally be
{{!-- my-details.hbs --}}
<button onclick={{details.toggle}}>
{{#if details.isOpen 'Close' 'Open'}}
<button>
{{#if details.isOpen}}
<div class='details__content'>
{{yield}}
</div>
{{/if}}
{{!-- some-other-template.hbs --}}
{{#my-details}}
the content that gets toggled
{{/my-details}}
That is, we separate the toggling behavior from what is toggled. That requires some way of passing the result of the content that gets toggled
to the MyDetails
object. For example, HTMLX might specify something like
If the value of the "as" expression is an object that defines a writeable
content
property, the HTMLX-compatible framework MUST evaluate the inner block, then set the content to the result. The framework MAY invokeset content()
after any update to the content.
as
is a possible addition — I haven't personally found a situation where it's necessary, because (in Svelte at least) you can use a computed property at the top level of a template, which basically serves the same purpose. I'd be tempted to make the opening and closing tags more symmetrical though:
{#with new MyDetails() as details}
<button onclick={details.toggle}>
{details.isOpen ? 'Close' 'Open'}
<button>
{#if details.isOpen}
the content that gets toggled
{/if}
{/with}
or
{#alias new MyDetails() as details}
<button onclick={details.toggle}>
{details.isOpen ? 'Close' 'Open'}
<button>
{#if details.isOpen}
the content that gets toggled
{/if}
{/alias}
More broadly, I wonder if control flow should be flexible enough to allow different frameworks to have their own opinions about this (e.g. Svelte has await blocks, which might not be interesting to other frameworks) — i.e. rather than having a canonical set of #if
, #each
, #alias
etc, everyone can pick the constructs that are suitable for their framework. Though that probably undermines the whole point of this endeavour.
As to your second point, most frameworks solve that problem with components:
<!-- MyDetails.html -->
<button on:click="set({ open: !open })">
{open ? "Close" : "Open"}
<button>
{#if open}
<div class='details__content'>
<slot></slot>
</div>
{/if}
<!-- SomeOtherTemplate.html -->
<MyDetails>
the content that gets toggled
</MyDetails>
+1 for with
. It at least resembles JS syntax and semantics.
I think you're right that we should maintain a component-centric mindset, but I don't think that absolves us of the need for with
or some sort of yielding. For example, in Ember Handlebars:
{{#my-list as |list|}}
{{list.item 'Foo'}}
{{list.item 'Bar'}}
{{list.item 'Baz'}}
{{/my-list}}
This allows components to render context-specific, encapsulated APIs for children. The alternative is to make every component global:
<MyList>
<MyListItem>Foo</MyListItem>
<MyListItem>Bar</MyListItem>
<MyListItem>Baz</MyListItem>
</MyList>
Or
<MyModal>
<!--
how can we have this button
(1) do some logic specific to our context, then
(2) call modal.dismiss?
If we're transcluded, we have access to the calling context, but no access to
the modal's methods.
-->
<button on:click="hmm()">
</MyModal>
compared with
{#with new MyModal as modal}
<button onclick={saveMyThing().then(modal.dismiss)}>
{/with}
Not sure I understand the point about global components? There's no reason a <MyListItem>
couldn't be local to the component that invokes it (indeed, that's the only way you can use a component in Svelte.)
I'm afraid I don't have a clue what that Ember syntax means! Can you explain what my-list
and list.item 'Foo'
etc are?
There's no reason a
couldn't be local to the component that invokes it (indeed, that's the only way you can use a component in Svelte.)
Interesting point! I hadn't considered lexical scoping in a markup framework. I guess that makes total sense.
my-list
is the name of a component. {{#my-list as |list|}}
opens the component's block and passes it (or whatever it chooses to yield) to the block. list.item
would require that the yielded object have an item
function. It could do anything, but most likely it's a partially-applied new MyListItem()
invocation.
Thanks. Ah, I think I completely misunderstood what your original proposal was getting at. I thought it was simply about aliasing values that are unwieldy to repeat, or that you don't want to keep evaluating:
{#with unnecessarilyLongObjectName.coordinates() as [x, y] }
<p>coords: {x},{y}</p>
{/with}
...not creating a new my-details
component. I don't think a new template language proposal should entertain the idea of creating components without angle brackets.
My own view is that content inside a component's 'light DOM' (to borrow WC terminology) shouldn't have any privileged access to that component — in other words these should differ only in the placement of the button, not in what the button can do:
{#if showModal}
<MyModal>
<button on:click="set({ showModal: false })">close</button>
</MyModal>
{/if}
{#if showModal}
<MyModal/>
{/if}
<button on:click="set({ showModal: false })">close</button>
The problem with
{#if showModal}
<MyModal>
<button on:click="set({ showModal: false })">close</button>
</MyModal>
{/if}
is that you've made how MyModal closes a responsibility of every caller of MyModal. If your modal has an animation, you want that to return a promise, then remove it from the DOM only after the promise resolves. That's why I like giving the caller access to modal.dismiss
.
There is, however, a way for these components to yield their API to the caller without any special syntax:
<MyModal on:inserted={(event) => this.dismissModal = event.target.dismiss}>
</MyModal>
<button on:click={() => this.save().then(this.dismissModal)}>
It's not as pretty, but it doesn't impose new requirements on implementers of the HTMLX spec.
is that you've made how MyModal closes a responsibility of every caller of MyModal
Not necessarily — it's up to the framework whether an #if
block becoming falsy means that its contents should immediately be yanked out of the DOM, or transitioned out gracefully.
See here for an example: https://svelte.technology/repl?version=2.3.0&demo=transitions-fly.
More to the point, without an #if
block how do you make the modal appear in the first place? I'd argue that a self-destroying component is an anti-pattern — but we're veering away from a syntax discussion and into something entirely else, so we should probably stop!
but we're veering away from a syntax discussion and into something entirely else, so we should probably stop!
Let me try a different approach then: why is each
special? Why is there
{#each foos as foo}
but not
{#with unnecessarilyLongObjectName.coordinates() as [x, y]}
The syntax currently has the concept of yield, but not in any arbitrary or extensible sense. Array
is far from the only Functor in the world.
Put that way, though, perhaps each
is just HTMLX for map
. As long as the foos
in {# each foos as foo}
implements map
it's an HTMLX-compatible Functor. A Nothing
would invoke the block zero times. A Maybe
would invoke it zero or one times.
In spec language, this might be something like
The argument to
each
MUST be a Functor (an object that has a readablemap
method). Implementing frameworks MAY place additional requirements on the argument toeach
.
each
is special, alongside if
, because conditionals and loops are sine quibus non — as long as you have those, you can do basically anything else:
{#each Object.entries(object) as [key, value] }
<p>{key}: {value}</p>
{/each}
{#each [...iterable] as item}
<p>{item}</p>
{/each}
Though as I said earlier with
could be added — I just haven't seen personally seen much evidence that it's necessary (and that's after implementing it in Ractive many years ago; it turned out to be pretty much wasted surface area).
HTMLx doesn't have an opinion about how expressions are evaluated though, and without that I don't think it's possible to say something like 'the argument to each
must be a functor'. In Svelte's case, the expression doesn't have to have a map
method, it just has to have a length
property, and other frameworks could have their own opinions about that:
https://svelte.technology/repl?version=2.3.0&gist=5e9d63b864d38b82b15af82d0c6925c8
In Svelte, isn't the following currently possible with #each
?
{#each MyDetails() as details}...{/each}
Sure, as long as MyDetails()
returns an array-like object
So would it be wrong to do..
{#each [MyDetails()] as details}...{/each}
.. to achieve as
behavior?
You’d spread the iterable into an Array
:
{#each [...MyDetails()] as details}...{/each}
See also, sveltejs/svelte#4289.
One feature that Ember's Handlebars offers is the ability for blocks to yield an object:
The HTMLX feature that comes closest is
{#each cats as cat}
. If you loosen the syntax just slightly to allow a JavaScript express instead ofeach cats
, but still only allow a single yielded object, the above could be written as