-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
let
it be: bind names to values in templates
#200
Conversation
let
keywordlet
it be: bind names to values in templates
what exactly it does without simultaneously holding both the `.hbs` | ||
and `.js` files together and continuously merging them with your | ||
mind. The lack of this overhead is one of the commonly cited strengths | ||
of JSX over Handlebars. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've always considered this to be a strength of Ember - not mixing invocation and implementation both in the template layer but keeping them apart. I generally think of the template context as an API for the template. When you use an API you don't want to deal with all of its internals but rather use it by invoking properly named properties and actions and assume it does the right thing under the hood. That takes a lot of complexity out of the template layer which is actually a good thing IMO.
I also gave a talk about the topic at last year's Emberfest: https://speakerdeck.com/marcoow/templates-and-logic-in-ember - hope videos will be up soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually read your slides very shortly after your talk, and I look forward watching the video!
requirement to explicitly enumerate dependencies. If the computation | ||
of a property has incorrectly specified dependent keys it will break | ||
in mysterious ways that usually involve a sudden and hard-to-reproduce | ||
lack of reactivity to changing inputs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a better approach to fix this is to make CPs easier. One way of doing so would be to come up with a special kind of CP that automatically tracks its dependencies. Doing that should (more or less) easily be possible, I just never found the time yet to build a prototype.
Also I think that computed properties macros can help make using CPs much easier for a lot of people and addons like https://github.com/kellyselden/ember-awesome-macros are doing a great job helping with that. /cc @kellyselden
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a better approach to fix this is to make CPs easier. One way of doing so would be to come up with a special kind of CP that automatically tracks its dependencies. Doing that should (more or less) easily be possible, I just never found the time yet to build a prototype.
You can not do this without code that comprehends the JavaScript AST of the function representing your property. That would be very cool, but would also come with its own complexities. Handlebars templates already comprehend the dependency flow at a syntax level and so we can lean on them for free.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can actuylly do. that by hooking into get
when running the CP function. You don't actually depend on all of the dependent keys all the time - actually you don't depend on anything that is not get
'd when computing the CP so e.g. you dont depend on anything that's accessed within
if (this.get('x')) {as long as
xis
false`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible, certainly, but is more and deeper magic the answer? Ember already plagued by problems like having the meaning of _super
be context sensitive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We built https://github.com/simplabs/ember-auto-computed as a proof of concept for the approach. Also I think @wycats implemented something similar for Glimmer.js' tracked properties.
|
||
This solves the problem of proximity by placing the derivation of a | ||
value right next to where it's used. There's no question about how the | ||
disabled attribute is computed. It also solves the dependency problem |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd argue the fact that you don't have to know how the value is computed when using a CP is a good thing actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this statement: Not needing to understand the computation is a good thing, and that is what assigning a name to a computation is about. You can think about the name, not the computation.
This is actually one of the problems I have with composable helpers:
<div class={{if (or isLoading isWaiting) 'pending'}}></div>
You can't look at this code and not understand the pending
class computation because it's mashed right up in your face. You don't have the option of deferring understanding until a time of your choosing, and that's not good.
The problem, is that when you do want to understand the computation, then it's time to do a name lookup and you are forced to search in a different file, controller, component, etc...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem, is that when you do want to understand the computation, then it's time to do a name lookup and you are forced to search in a different file, controller, component, etc...
I honestly don't think this is an actual problem that people have - especially since there's only ever one JS file per template that you'd have to look into.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem, is that when you do want to understand the computation, then it's time to do a name lookup and you are forced to search in a different file, controller, component, etc...
that's also something that can be improved by working on better tools that allow references in templates to be resolved
value right next to where it's used. There's no question about how the | ||
disabled attribute is computed. It also solves the dependency problem | ||
because the dependencies of an expression are implicity enumerated | ||
merely through the act of expressing it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Helpers don't alway behave as expected though. While they observe the values of their arguments they don't observer the value of any properties on the arguments. E.g.
<div>{{fullName user}}</div>
where fullName
is
function([user]) {
return `${user.firstName} ${user.lastName}`;
}
will not update when the user's firstName
or lastName
properties change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this is as a problem generally with using shared mutable state. Unfortunately, this is still the common Ember practice, and in practice what you see all the time is users only specifying the base object and not attributes in their computed properties.
The way we write apps today with an immutable style, the fullName
helper you describe works just fine, because a change to user.firstName
means that the reference to user
changes as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which is to say that I think this is orthogonal to the scope of this rfc
{{/let}} | ||
</div> | ||
a + b = {{sum a b}} <!-- 3 --> | ||
{{/let}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm honestly worried that we'd start seeing templates like this in client projects
I think the overarching question as @marcoow has already pointed out is how much logic you want to see in your templates, or broadly speaking how the relation of template and component/controller should look like, in terms of responsibilities and concerns. And I am not sure the Ember community has found a consensus there (yet). On one side addons like your Also don't know what implications that would have on the rendering engine, seems there would be quite some managing of state required (e.g. you would not be able to just rerender the nested I think there is already some very reasonable policy in place in the Ember community of first trying new things out in addon-land, and when this has been explored and proven and became popular this way, then this can be integrated into core. So, if we see a broad adoption of |
@simonihmig I'm not sure I follow. Imperative constructs just don't exist in handlebars, and I think we all can agree that's a strength. |
I don't think there will be any need to change the rendering engine. @marcoow @simonihmig As for best practice, I'm not prescribing that users should always declare their computations in their templates, or that you should not use computed properties anywhere, but I think it's also important to acknowledge that the persistent popularity of addons like ember-composable-helpers demonstrate that a large proportion of Ember developers already feel the pressures I'm describing and have for some time Many of the problems with using composable helpers today stems from the fact that they are used to generate large, anonymous, and therefore opaque expressions. Not wanting those in your code base is a good thing, and so I see |
I was referring to your inline style, that kind of gave me a feeling of imperative code, in the sense that the order of statements is of importance. It could give the impression that the template is kind of executed line by line from top to bottom. I think this implicit scope of the inline style could lead to confusion. I did not have this issue with the block style. But maybe it's just me... |
After rereading your proposal, do I understand you correctly that you want this... {{let a=1 b=2}}
a + b = {{sum a b}} to be implicitly transformed to this one: {{#let 1 2 as |a b|}}
a + b = {{sum a b}}
{{/let}} probably at build time? So the latter is the only thing that Glimmer2 or whatever the rendering engine is will see? Then I can follow you. I thought the rendering engine would have to manage the implicitly created scope somehow... |
@simonihmig - Yes, and FWIW that is how ember-let works today (inline let is transformed to block form at build time). |
I am a strong believer in two things regarding templates and separation of concerns:
I am a happy user of I want to throw out there an element of the Liquid templating language which I'm quite fond of which might be worth taking a peek at: assign. I've used Liquid pretty extensively, and assign is a tool that is one of its most powerful features Liquid has. It can also be brutally abused. In Liquid, assigned variables are available to anything in that template, but aren't available to any sub components unless they are passed to that component explicitly. I'm not clear on the ins-and-outs of the scope of those assignments, but that's the general idea. I'm interested to see the direction this RFC goes... I've already learned about some pretty fantastic addons i wasn't familiar with 💯 |
For users looking for the very basic functionality that's described here, here's a method that already works: {{#with (hash
one = "One"
two = "Two"
three = "Three"
) as |h|}}
<p>I've been to {{h.one}} beach, {{h.two}} coffee shops and {{h.three}} bars.
{{/with}} I think this should be added to the alternatives section. |
@lolmaus The current alternatives section has this to say:
I reworked it to include you example. Does it seem more clear now? |
e7b2077
to
87fa909
Compare
@cowboyd Thank you. In Alternatives, you say "see caveat_s_ in Detailed design", but Detail design contains only one caveat. You literally say "The only difference". Being very explicit about this will make it easier to understand the value of this RPC. I can now see three benefits:
I personally have doubts. The block form separates names from values, making it hard to read and edit. Consider this synthetic example: {{#with
(some-math a b c)
(some-math b a c)
(some-other-math b d)
(some-math c a b)
(some-other-math d d)
(some-other-math d a b)
as |topLeft bottomLeft bottomRight topRight center focus|
}} And now you have to update As for the inline form, it turns purely declarative, functional, Lisp-like templates into imperative ones. When you see an unfamiliar variable in your template, it's no longer enough to look up the hierarchy for a Ideally, the compiler should be smart enough to merge multiple inline Sometimes restrictions are good, and Handlebars are restrictive on purpose. Maybe we should rather upgrade the |
@lolmaus When I say caveats plural, I mean
I disagree. There are absolutely no side effects with either the block form or the inline form since the inline form is transformed to the block form via a macro (itself a pure function). let x = 5, y = 2 in
x + y But we don't say that OCaml is imperative because unlike JavaScript To your point though, I think that it should not be just a warning, but a hard error (as it would be in OCaml) to redefine variables at the same level of scope. |
@simonihmig I see your point as the inline form is probably the most radical departure from the way people normally think of scope in handlebars, and while there is precedent for this style in other non-lisp functional languages, I'm considering decoupling it from this rfc. That said, it does prevent a lot of rightward drift when you have many values. |
@cowboyd Yes, it's just the inline form I have some concerns with. Just let me clarify this a bit more.. I think it is important to have the user in mind. And when we want Ember to not loose ground on other frameworks, we should focus on new and novice users and improve the DX there. I guess everybody who reads this will have a fairly decent knowledge of Ember and will adapt to new things easily, even if done in a weird way (not saying this is weird). But that should not fool us to think that this will be true for everybody else... So having introduced Ember to new teammates myself, I know some issues they had. For example some were tempted to do something like this: {{#if this.isEnabled()}}
<p>{{this.getMessage()}}</p>
{{/if}} This is an extreme example, just made up to clarify the underlying problem. While you don't have to know the details of how things work under the hood, you have to have the right mental model of how things work in principle. And in this case they were thinking in an imperative way, that is they assume they control the flow of operations and are in charge. While they are not. This is especially true if they have some background in other languages like PHP that allow to be used as template languages itself and mix markup and code. An example similar to the one above would be possible there, as it would be actually executed imperatively. But in case of Ember and Handlebars, the better mental model would obviously be a declarative one, that describes what the solution should look like and not *how * it should be done. And maybe that of some kind of inversion of control, where the framework controls the flow of operations, and calls you (lifecycle hooks, CP) when needed. While the inline |
I've started an alternative RFC proposal: Allow the |
FWIW some comments from my side:
|
The binding would be available from the
☠️ |
I have a feeling code like this would start popping up if we allowed inline let // variable declaration area
{{let foo1=(bar ....)}}
{{let foo2=(bar ....)}}
{{let foo3=(bar ....)}}
...
// now start html area
<div>
... Which brings us into a quasi js world where our imports/consts are at top, then we start coding. More of a visual problem for me instead of any technical problem. |
Hi everyone! I published an RFC that focuses on the block form of the helper: #286. From the great discussion in this RFC, I thought it would be useful to split it up so we can incrementally ship this feature, and go into more detail into a potential future RFC for the inline form. |
Closing as #286 partially superseded this proposal. |
Rendered