Thought Experiment: Partially-Applying Ng-Template References In Angular 7.2.13
Yesterday, I ran a sanity check that nested Ng-Template references maintain lexical binding in Angular 7.2.13. It was nice to see that my "closure"-like mental model was sufficient. But, it got me thinking: what if an Ng-Template reference needed to consume a variable that wasn't in its lexical scope? Could we use the NgTemplateOutlet directive to "partially apply" variables to an Ng-Template reference in Angular 7.2.13? Short answer: Yes; Long answer: you should probably never do this - it is pure madness!
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
I am very poor at the concepts of Functional Programming (FP); so, please take the following explanation with a grain of salt. With that said, the idea behind "Partial Application" is that we can take a Function that accepts some number of arguments and return another Function that takes some fewer number of arguments. This new function will then implicitly provide the arguments that are needed by the original function but that are missing from the new function's signature.
A pseudo-example:
Partial( F( a, b, c ) ) => F2( c )
Here, F() requires 3 arguments and F2() requires only 1 argument. This is because F2() is applying the first 2 arguments - (a,b) - internally, allowing the calling context to only provide the last, non-applied argument (c).
With Ng-Template, we can do the same kind of thing using the NgTemplateOutlet directive. The NgTemplateOutlet directive allows us to render a TemplateRef while also providing a context object that can populate any required template variable. Now, if we wrap the NgTemplateOutlet in another Ng-Template, then we can duct-tape together something that kind of looks like partial-application:
NgTemplateOutlet( TemplateRef( a, b, c ) ) => TemplateRef( c )
To see what I mean, let's look at a concrete example. In the following Angular component template, I ultimately need to provide a template to an [NgForTemplate] input binding. This template expects the "$implicit" and "index" variables exposed by the NgFor directive; but, it also expects two other variables, "let-arg2" and "let-arg3", which we will need to partially apply to the rendering:
<!--
To start this experiment for the partial-application of templates, we're going to
start out with a template that will ultimately act as an [NgForTemplate] renderer.
This template will expect the implicit" and "index" variables from the [NgFor];
but, will also look for several other variables, "arg2" and "arg3", that we will
partially-apply to the template before we pass it to the [NgForTemplate] binding.
-->
<ng-template
#ngForTemplateRef
let-implicit
let-index="index"
let-arg2="arg2"
let-arg3="arg3">
<p>
( {{ index }} ): {{ implicit }} , {{ arg2 }} , {{ arg3 }}
</p>
</ng-template>
<!--
STEP TWO: Partially-Apply "arg2".
--
In order to partially-apply the "arg2" variable (ie, hard-code it), we'll need to
define all arguments, but hard-code "arg2" as we "render" the previous template.
-->
<ng-template
#partialTemplateRef
let-implicit
let-index="index"
let-arg2="arg2"
let-arg3="arg3">
<ng-template
[ngTemplateOutlet]="ngForTemplateRef"
[ngTemplateOutletContext]="{
$implicit: implicit,
index: index,
arg2: 'applied-arg2',
arg3: arg3
}">
</ng-template>
</ng-template>
<!--
STEP THREE: Partially-Apply "arg3".
--
In order to partially-apply the "arg3" variable, we'll need to define all the
non-applied variables from the previous template, but hard-code "arg3" as we "render"
the previous template. Remember, since "arg2" was hard-code above, we don't need to
include it here.
--
NOTE: We could have combined this step with the last step; but, I am breaking them
apart to make the partial-application a bit more interesting.
-->
<ng-template
#partialTemplateRef2
let-implicit
let-index="index"
let-arg3="arg3">
<ng-template
[ngTemplateOutlet]="partialTemplateRef"
[ngTemplateOutletContext]="{
$implicit: implicit,
index: index,
arg3: 'applied-arg3'
}">
</ng-template>
</ng-template>
<!--
STEP FOUR: Consume Partially-Applied Template.
--
Now that "arg2" and "arg3" have been partially-applied to our original template,
we can consume the partially-applied template. It will naturally pick-up the
"let-implicit" and "let-index" variables as those are provided by [NgForOf].
-->
<ng-template
ngFor
[ngForOf]="[ 'foo', 'bar', 'baz' ]"
[ngForTemplate]="partialTemplateRef2">
</ng-template>
As you can see, the first template expects two standard NgFor variables and two non-standard template variables. In order to prepare the template for consumption in the NgFor loop, we need to apply the non-standard template variables such that they won't need to be provided by the NgFor rendering.
To do this, we're going to "wrap" the Ng-Template inside of another Ng-Template that renders the first TemplateRef while also hard-coding some of the template variables. Of course, we could replace the hard-coding of the variables with some lexically-bound references; but, for the sake of simplicity, we're just hard-coding the partially-applied values.
Once we partially-apply all of the non-standard NgFor variables, our NgFor directive is able to consume the original template. And, when we run this Angular application in the browser, we get the following output:
As you can see, the "let-value" and "let-index" are successfully consumed from the NgFor context; and, the "let-arg2" and "let-arg3" are successfully consumed from the partially-applied contexts.
So, this totally works. And, is - at least to me - an interesting exploration of the Ng-Template and NgTemplateOutlet constructs in Angular 7.2.13. It's nice to see how flexible Ng-Template is, especially with its lexical binding. That said, I think this safely falls under the category of "clever code"; and, should probably be avoided in most circumstances.
Want to use code from this post? Check out the license.
Reader Comments
@Ben Once again great content! There are so few resources online that dive as deep in to Angular as you do.
Do you have any blog posts on your motivations? For example you say "this is some mad sciency kind of stuff, I think it's kind of fascinating". First of all I agree ??. Second, is it the fascination that drives you to create these blog posts or are you learning by teaching? Or is it mainly another motivation?
In general, I would like to hear more about your philosophy and attitude because I am very impressed by your achievements, consistency and competency. Thanks, Tom
I have read your about me but I still want to know more :-)
@Tom,
It's a great question. I think I have written about bits and pieces of this. For example, a few years ago, someone asked me about how I deal with the fast-paced JavaScript landscape. This was my response:
www.bennadel.com/blog/2994-on-staying-focused-and-productive-in-the-world-of-programming.htm
Of course, it can all be overwhelming. In fact, I just yesterday was writing about a mental-connection I made between my slow-pace of adoption and my love of what I call the old-man vengeance underdog in movies:
www.bennadel.com/blog/3635-romanticizing-the-idea-of-old-man-vengeance-in-a-fast-paced-web-development-world.htm
At the end of the day, writing stuff down really helps me remember things a lot more effectively. And, when I can't remember the details, I at least remember that I knew the details at one time; and, that I can look up the finer-points when I forget.
And, for the "mad sciencey" stuff, I just get curious. If the thought, "I wonder if this would work..." ever pops into my head, I just feel like it's an itch I have to scratch :D And, sometimes, in doing so, I stumble upon insights that give me other ideas. Mostly, I'm just playing and taking joy in the exploration of it all.
why line 61 and and 31 even required?
@Ben - The first article you linked to had me smiling most of the way through. There are such gems in there, that I felt it my obligation to submit it to hackernews. I also beleive calluses and never made the connection between them and tooling. Thank you very much for your insights.
You seem very well read. When you refer to 'strategies' at the end of the post, I read 'principles' instead. As in Ray Dalio's book 'Principles'. If you haven't read it already, being quite similar to you, I can highly recommend it to you.
@Rick,
Sorry, that part is a bit confusing. I tried to explain it in the post, but on second reading, it wasn't the most clear:
For the demo, I didn't actually have any real demo data to work with. So, instead of passing the
let-*
args through, I am just hard-coding the values. So, the lines that you pointed out, I am putting there because they represent the "template signature" (so to speak); but, I am not actually consuming them in the demo - I am hard-coding the values that would have been references to thoselet-*
values.@Tom,
I'm glad that some of it connected. It's always changing a bit; but scanning through the sub-headings, I'm basically same now that I was then. I always wish that I was brave enough to do larger exploration; but, I think part of me is afraid that I won't have anything to show for it. But, that's too much ego-driven learning -- I need to get better at embracing learning that may not yield things I can write about.
I've not heard of Ray Dalio, but he sounds interesting. And, it looks like he's done a TED talk (love TED talks!). I'll have to check it out.