We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Why JavaScript is Coming to Ember Templates

00:00

Formal Metadata

Title
Why JavaScript is Coming to Ember Templates
Title of Series
Number of Parts
24
Author
License
CC Attribution 3.0 Unported:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Identifiers
Publisher
Release Date
Language

Content Metadata

Subject Area
Genre
Abstract
In March 2018 the Ember project announced the abandonment of a long-running effort to improve Ember's dynamic resolution system (aka "Module Unification"). We didn't do it, and now we're going to talk about why. Soon, Ember developers are going to be seeing JavaScript in the same files as Ember templates, which opens the door to a new way to organize component implementations.
Linker (computing)RhombusMusical ensembleComputer animation
Shape (magazine)CodeSoftware frameworkTemplate (C++)Slide ruleGoodness of fitMilitary baseCodeMoment (mathematics)Self-organizationMereologyCore dumpProjective planeJSON
MultiplicationModule (mathematics)Compilation albumComputer fileLecture/ConferenceMeeting/Interview
Component-based software engineeringEuclidean vectorRun time (program lifecycle phase)Cartesian coordinate systemUniform resource locatorMobile appImplementationReading (process)Connectivity (graph theory)Mathematical analysisMaxima and minimaIntegrated development environmentType theoryLogicDifferent (Kate Ryan album)MereologyFluid staticsHydraulic jumpComputer fileRange (statistics)Resolvent formalismNumberPhysical systemTemplate (C++)Software developerFocus (optics)MiniDiscIntrusion detection system
Maxima and minimaConnectivity (graph theory)Image resolutionPhysical systemArtificial neural networkReal numberMaxima and minimaModule (mathematics)Matching (graph theory)Hill differential equationMedical imagingCASE <Informatik>Formal grammarRule of inferenceComputer fileSoftwareMeeting/Interview
Component-based software engineeringFunction (mathematics)Image resolutionVolumenvisualisierungTemplate (C++)Level (video gaming)Mobile appMoving averageConnectivity (graph theory)Multiplication signMathematical analysisCodeString (computer science)Presentation of a groupLogicModule (mathematics)Computer programmingBitNumberComputer fileComputer wormRevision controlLevel (video gaming)OpcodeRun time (program lifecycle phase)VolumenvisualisierungSystem callFunctional (mathematics)Library (computing)BuildingTemplate (C++)Dynamical systemSoftware frameworkCompilerSingle-precision floating-point formatPhysical systemFunction (mathematics)Link (knot theory)Resolvent formalismImplementationLattice (order)Lecture/Conference
Image resolutionCompilation albumCodeServer (computing)Connectivity (graph theory)Codierung <Programmierung>Template (C++)String (computer science)Context awarenessMathematical analysisFormal languageLibrary (computing)Integrated development environmentComputer programmingResolvent formalismCASE <Informatik>Compiler
Image resolutionVolumenvisualisierungTemplate (C++)Component-based software engineeringPhysical systemOrder (biology)Cartesian coordinate systemString (computer science)Connectivity (graph theory)Image resolutionOpcodeDynamic rangeSoftware developerMultiplication signResolvent formalismRevision controlDirection (geometry)Rule of inferenceContrast (vision)Electronic mailing listModule (mathematics)Library (computing)Scripting languageTemplate (C++)BootingLevel (video gaming)Lecture/Conference
Fluid staticsLattice (order)Function (mathematics)Asynchronous Transfer ModeOpen setConstraint (mathematics)Image resolutionEuclidean vectorPartial derivativeBlock (periodic table)Template (C++)Regular graphVolumenvisualisierungDefault (computer science)Source codeVertex (graph theory)Physical systemSoftware frameworkMeta elementCompilation albumConnectivity (graph theory)CASE <Informatik>CodeTemplate (C++)Hydraulic jumpInstance (computer science)Electronic mailing listPoisson-KlammerStatement (computer science)Dynamic rangeComputer fileAngleFunctional (mathematics)Message passingVariable (mathematics)TwitterSheaf (mathematics)Scripting languageRevision controlFormal languageCompilation albumGreatest elementBitModule (mathematics)Partial derivativeParameter (computer programming)Software developerConstraint (mathematics)Default (computer science)Computer programming2 (number)Entire functionImplementationOpcodeLink (knot theory)Different (Kate Ryan album)String (computer science)Asynchronous Transfer ModeMobile appSource codeOrder (biology)Category of beingRun time (program lifecycle phase)MappingSoftware frameworkSelf-organizationMultiplication signBlock (periodic table)Directory serviceComputer configurationLocal ringFunction (mathematics)Cartesian coordinate systemDynamical systemError messageImage resolutionState of matterVolumenvisualisierungFactory (trading post)Right anglePhysical systemWritingMiniDiscTheory of relativitySampling (statistics)Group actionSet (mathematics)BuildingProcess (computing)Lecture/Conference
Asynchronous Transfer ModeFrequencyBuildingTemplate (C++)Module (mathematics)outputPoint (geometry)Connectivity (graph theory)Stability theoryProcess (computing)ImplementationFeedbackBitFrequencyCASE <Informatik>outputRevision controlOrder (biology)Data managementModule (mathematics)Template (C++)Asynchronous Transfer Mode
Template (C++)Fluid staticsComputer wormSoftware developerResolvent formalismTemplate (C++)Group actionFluid staticsElectric generatorConnectivity (graph theory)Natural numberProjective planeRule of inferenceNumberLocal ringModule (mathematics)Meeting/Interview
Slide ruleSoftware frameworkModul <Datentyp>Reading (process)Link (knot theory)Web pageTemplate (C++)Projective planeModule (mathematics)Slide rulePresentation of a groupShared memoryVirtualizationOnline chat
Transcript: English(auto-generated)
I'm Matthew Beal, and I'm excited to be speaking with you here again at EmberConf. Before I start my talk here, I just want to say I've been incredibly inspired by the work that Tilda, the conference organizers, my fellow speakers, and many others have done to make this conference virtually feel like a success. My heart and thanks go out to each one of you for the work that you've done to
make this feel like a real moment for our community. So last year, I started a new role in engineering at a company called AtaPar. We're mostly based in New York City. We work on one of the longest maintained Ember code bases that's out there. And additionally, on a couple of new code bases using Ember Octane. In a big project like ours, you quickly learn to prioritize writing code that's easy to read.
And that's one of the reasons that this topic today is important to me. Okay, so to dive in here, there's a joke on the core teams. It goes, no more unification RFCs. And yeah, that's the punchline. Maybe some of you have an idea why. Just before EmberConf last year, we decided to withdraw this RFC,
the module unification RFC, from consideration. It might seem late to be talking about this topic, and I'm not going to talk about module unification in detail. But what you should understand, if you don't already, is that module unification was a last attempt at cleaning up some loose ends in how we organize and reference files in Ember. So let's say that we're reading this template in an application.
Most Ember developers would intuit that when they look for the definition of welcome on disk, they would look at this location in app components. And that's often the correct answer. But if you want to make an implementation of jump to definition, say, so that when you hover on welcome, you can jump to the file where it's defined. You would actually need to consider a much larger range of possible places
that that thing can be. And the same thing would go for TypeScript, if you wanted to let TypeScript find the definition of that file. Ember uses a resolver system that permits components to be defined in a number of different locations in your app or in an add-on. And the logic for deciding which to use is implemented as part of your application's runtime. Now that makes it challenging to support common static analysis tools,
things in IDEs and type systems and bundlers that people use across other parts of the JavaScript community. You might already be familiar with the concept of a local maximum. When you're looking for a solution to a problem, there are probably many viable solutions to that particular thing. However, the path from one solution to another solution might not always be
linear, and so it can get easy to over-focus on the thing that you have in front of you instead of looking at the whole range of design possibilities and solutions that might be out there. You'll often read about this idea when you dig into something like neural network systems. For example, this neural network that generates horses on this
horsesnotexist.com managed to generate this really realistic image of a horse on the right-hand side as a global maximum. But in other cases, such as the one on the left here, it was equally confident that it had found the right answer, but it had completely missed the mark. So I think that modularification was probably a local maximum. We had identified places like real problems where Ember's resolution system
wasn't always clear about what file you were looking at, what component you were trying to invoke. And modularification introduced more formal rules and requirements in an attempt to unify that system. In hindsight, though, it was a local maximum, and the rest of the JavaScript community was climbing the hill to a better solution. This talk is about how we're going to align Ember with that better solution.
So to illustrate the problem with Ember's resolver system, let's build a little fork of Ember I'll call mats-resolving-ember-microlib. Here's a component in my framework, and you can see it's just a function that returns a string. And we're going to presume that it's in a file here called components.js.
And here's my framework. I know this looks like a lot of code. The most important bit to look at here is on the bottom, we're calling a render function, passing it a template. And then further up, we've got a template itself. Now there's two different things that we call opcodes in there that say how I want to render this template. The first one is the number 0, which says I want to append text. And the second one is the number 1, which says I want to invoke this component.
And if you look at the render function above, you can see how those things are done. And further up, there's a setup where we create a map of all of our components, and then we render them. So let's see how the ambiguity of using this system, because we're using a string for welcome here. Let's see how the ambiguity presents problems where a lot of tooling can't
penetrate the meeting. So first, I've run this program through some popular build tools, Rollup and Tursar. Rollup takes advantage of the fact that ES modules are static. That is, the imports and exports from a module can be understood without running the code. Rollup figures out how to take your multiple modules and safely combine them into a single JavaScript program.
You can think of it as a simple compiler which takes our dependency, the component, and links it to the main program. Tursar is a minification tool which uses static analysis to make your JavaScript payload smaller. For this version of the program, the output looks like what we would expect. The component here is present, and the rendering logic is present. And you can see our template in there with the opcode 0 and the opcode 1 as well.
Let's go ahead and change our template. So instead of calling opcode 1 to print the component here to invoke our component, let's just render more text with an append opcode instead. So now I have two text opcodes of 0 to UISay, a FOND farewell. And this would be akin to you opening up a template in your Ember app and deleting the invocation of a component to replace it with some text.
So in this output, again, running it through Rollup and Tursar, you would expect the component logic doesn't need to be present since we stopped referencing it in the templates. But because their relationship was something resolved at runtime and because Rollup and Tursar don't know what the program will actually do,
they aren't able to strip that component out. In the Ember ecosystem, we're working on tools like Embroider. Embroider tries to close this gap by teaching the build tools to assume things about the runtime during build time analysis. But I can give you another example of how the resolver's dynamic implementation will frustrate other tooling.
So finding a second example of how this ambiguity in our resolver-based library has practical impacts is as easy as looking at the most popular IDE for Ember users, VS Code. Here I've opened up the microlib, and I'm attempting to jump to the definition of a component just like it was referenced inside of our template. So it isn't surprising that this doesn't work, right?
You can actually read it with your eyes here. We have a string for welcome. And just like we don't, without understanding the program, have any context for what that string means, our compilation tools don't have that, and our analysis tools in this case, don't have that understanding themselves. We could, again, teach the tool about this. We could build a custom language server and encode assumptions about where to look for these definitions.
So the first draft of my microlib used dynamic resolution to look up components, the application boots, the available components are put in a map, and then templates reference them by strings. In order for our eyes to know where to find a definition and for our tooling to know, we need to teach the systems what those rules for resolution are.
And I think that that teaching also comes across for us as individual developers. When I asked you to look at this template and tell me where the welcome component is defined, you could probably give me a pretty reasonable answer, but that's because you've internalized the rules of the resolution system. In contrast to that, in a static system, one based on ECMAScript modules, we're always going to be able to be explicit
about where a definition comes from. You don't need to teach any tooling or people those rules. So let's build a second draft of the microlib, this time a static version. So in this version of the template, I've referenced the welcome component directly. Down in our template, we have our opcode zero with our string to append. We have our opcode one with the actual argument of welcome,
which is the thing that's imported from the top. There's no setup, there's no list of components, there's no resolver here. Furthermore, if you want to see where the welcome component comes from, you can read this really easily, right? You just look at where it's imported from. So now that the template contains a direct reference to the component, we don't need to teach the tooling about any ambiguous cases.
So jump to definition just works in a template the way that it would work in most JavaScript code. And here we've got that running. And when the bundlers process this, again through rollup in Tursor, the implementation of the component is actually lifted directly into the template itself. So we can see our opcode zero to USA,
and then our opcode one with the actual component itself inlined into the template. We've made the link between the program and the component static. The tooling can not only understand where the component is being used, but it can also understand if it's not used at all. For example, if I change the second render step back to a fond farewell as an append instead of a component invocation,
the bundler understands that the variable welcome was not referenced, and the entire component implementation itself can be dropped. Okay, so Ember's templates are of course a lot more featureful than my microlib. So how can we bring the benefits of a statically linked system back into Ember itself?
So Godfrey Tran has been exploring this in RFC 496 here. In that RFC, he proposes strict templates in Ember. A strict template mode. We call it mode because just like strict mode in JavaScript, it ops the user into a version of language where messy edge cases are going to be disabled. So what's going to make a strict mode template a strict mode template?
So really it's going to be a list of things that we apply as constraints, again to remove ambiguities from the system. So the first of these is that there's going to be no more implicit disk fallback. So if you invoke something or if you access something like curly curly foo here, we're not going to look to this dot foo. If you want to access the component instance, you must use this dot.
This is something that's an octane is already linted for, so that's pretty straight ahead. There's going to be no resolution of any invocations. So for example, curly curly foo dash bar is a component invocation. Our angle bracket welcome was a different kind of component invocation. In a strict mode template, those things won't look up components
in the app folder of your program. So this is obviously something that we're going to have to come back to. There's no dynamic resolution. So you can't use the component helper to pass a dynamic string here because this is not analyzable at all. You could pass any string at runtime and there's no way for us to know what you might pass in. And then partials have a similar facility
where they can also take a dynamic argument. Partials are already deprecated. So again, this is pretty much already on the happy path. Okay, here's an example of what a handlebar's strict mode template could look like. It shouldn't look very different than an Ember template you might work in today. It's going to contain a couple of template keywords.
So in this case, we have each being used in here. Arguments, so at greetings is an argument to this component that I'm using. It has a block param my greeting that we're then using inside of the block. And we're accessing properties off the component state. And that works just like it would in any regular template. However, there's no actual way to invoke a component from app.
So for example, if I had put bracket like angle bracket quote in here, it wouldn't actually render a component at all. It would just be an error. And so this brings us to the crux. In order for these static templates to actually be useful, we're going to need a static solution for getting other components into the scope of the template. Since we want to bundle our application as JavaScript,
we want to do something that's going to work with ES modules, of course. And to work with ES modules, we need to consider what a strict mode template looks like when it's compiled into JavaScript. So here's that same strict mode template compiled into JavaScript. Only I've changed it this time to invoke the quote component around my greeting. To bring that component into scope,
I've imported it from a file that's in the same directory as our component here. Just quote, you can imagine that it exports an Ember component, Glimmer component. Just like with my static microlib earlier, the definition of the component itself is now passed directly into the compiled template. You can see that down where we have a property of scope and then we're passing a function with quote being closed over.
Common build tooling would work well with this output and given some work on source maps, we can make jump to definition work as well through the template itself. Of course, this API is basically as far as the handlebar strict mode RFC itself goes. And this API is only a primitive, right?
We don't write compiled templates by hand. In order to make this readable and usable, we need to find a way to lift the import statement that we're writing in JavaScript here into the template itself, which is going to take us into a design for template imports. So if we want the ability to bring other components into a strict mode template scope,
there are a couple of different plausible designs that we could go into. To keep the design kind of unsurprising to both new developers and advanced Ember developers, we think that two constraints are important to keep in mind. The first is that you want to be able to import a default or a named export. So you want to be able to say that my component from elsewhere or my helper or my modifier
is the thing which I've named arbitrarily in my exports and I can pull it into my template. And the second thing is that on the right side of an import, the path, we want it to work just like it would in any Node.js resolution. Nothing special to go on there. So given that we accept these constraints, it's going to beg a question. If we're going to constrain ourselves to so closely re-implementing
the constraints already provided by ES modules, why not simply adopt the ES module syntax into templates themselves? In fact, any design that doesn't do that seems to bend common sense a little bit too far. So let's see what that's going to look like to bring the ECMAScript module syntax into templates.
So a strict mode template itself doesn't have imports, but if we bring in a design for template imports, we can bring them into scope here. So we have dash dash dash, import quote, and then another dash dash dash. The dash dash dash introduces a preamble where we're going to be able to write our import statements. And at the bottom, we have our template.
In between the preamble section, we want to constrain what's available in there to only be ECMAScript module syntax. So you can't use variables, you can't define functions or anything like that. And that's really as a first pass. Imports can, of course, come from any path because we want the thing on the right side of an import statement to work just like it does in normal JavaScript. So here we're importing quote,
which would be a file that's in our local application. We're also importing titleize, which is a helper that the Ember framework itself is providing. And then we're importing animated each from an add-on. Imports are going to open up new organization options for your app. If you have a naturally grouped set of components, you can group them on disk wherever it's going to seem appropriate.
Additionally, related helpers and modifiers could easily share a single file, but be exposed as named imports, for example, as we're doing for the add-embr slash template helpers module here. So how do these imports compile back into JavaScript? Because this is just our syntax in the template. So just as with the handwritten compilation from earlier,
these three components are passed in inside of the compile template, closed over from the imports. The imports from our template are lifted into the JavaScript here. Then we close over the values that are imported and pass them into the create template factory itself. And this is great. It means that when we look at the compile JavaScript here, it's easy for our eyes to understand
where a quote is coming from, which means it's also easy for our tooling to understand. Okay. So what are the next steps in the process of delivering template imports? So before Glimmer components had landed in an Ember stable release, we actually landed a primitive into a stable version of Ember component manager. This allowed us to implement Glimmer components
inside of an add-on and circulate them amongst like the most passionate users who are eager to try and experiment with that solution and to get their feedback. That feedback kind of helped us shape the Glimmer components feature until the point where we thought it was really ready for a stable release. We want to do the same thing in this case. The handlebar strict mode is an RFC
that we have to get into a final comment period and land that primitive. That will then allow us to build an add-on on top of it, which provides the template import syntax. Early adopters can experiment with that and let us know what needs to be shaken out in order to make it a success. Kind of in parallel, we're also going to need to do a little bit of design around what the ES module API would be like for things
like linked to or input. It's probably a little bit of extra design work to do there. So I've talked a lot about the technical nature of static templates in this talk, but despite the fact that static templates will have performance in payload size impacts, I want to remind you that performance isn't the headline motivation here.
Static templates with ES imports make it a template simple to understand for our eyes as well. So in this first example, we have welcome where you as an Ember developer need to understand how the resolver rules work. But in the second one, even if I've never used Ember before, it's really clear to me where this thing is coming from
and where I need to go to find the definition. Additionally, module unification had some interesting features you might've heard of like local lookup. Explicit imports make those features pretty much unnecessary. An import syntax will just allow you to group components naturally in your project without losing the common conventions as suggested by linting and generators.
So that's why JavaScript, or at least JavaScript import syntax is coming to Ember templates. So the Ember project was one of the earliest adopters of ES modules and embracing the opportunities that ES modules present in making our templates more readable and better analyzed is something that I'm pretty excited about. But more than that, it's going to give us the opportunity
to take challenges that Ember has in common with other JavaScript projects and allow us to better share common solutions between them. If you want to do further reading on this topic, I encourage you to take a look at the links that I've put on the page here. And I've also included bit.ly so that you can open up these slides yourself this afternoon. Thanks for joining us here at EmberConf virtually.
And I look forward to talking about this topic more with you on chat. Thanks.