title | author |
---|---|
Using can-import to Explain The Special Features of Stache and Stache Bindings |
Bradley Momberger |
Recently on Bitovi's internal Slack, one of our developers wanted to have a better understanding of a can-import
tag that had some extra fancy sauce applied to it. Lately at Bitovi we've been working hard on reducing both special cases and arcane syntax in Stache, to create a clear and consistent experience for template creators. Even with these efforts, it can sometimes not be clear what something does when using all of the available features. So to help motivate better understanding, here's an illustrative example of how can-import is consistent with other tags and references in Stache. For the can-import
tag in question, I've highlighted things with special meaning.
Overall, the purpose of this tag is to import a Stache file to be used as a partial, then render the partial along with the rest of the script. The relative path to the Stache file is "./example.stache", which explains the from
attribute. The rest of the tag is decorated with a few odd extra characters. So let's explore those funky bits and what they mean, and why they're all essential to making this work correctly.
When a tag or template uses its own view model rather than one from its parent, Stache uses bindings to connect data between parent and child. The bindings have a sytnax that determine which direction the data goes (whether setting A sets B, or setting B sets A, or both). That is the meaning of :to
here: the value of value
from the child (the can-import
tag) gets pushed to the attribute value on the parent (the template that contains the tag).
The attribute is called value
and it's reasonable to ask at this point whether value
has a special meaning.
The answer is yes... kind of. What's important to know here, is that the view model for a can-import
tag is the Promise that resolves when the import has been loaded. With can-reflect-promise
, all Promises get special keys that can be read in Staches:
isResolved
/isRejected
/isPending
(state-related predicate functions)state
(one of "resolved"/"rejected"/"pending")value
(the resolved value, if resolved)reason
(the reason for rejection, if rejected)
So value
in @value:to
is reading from the resolved value of the Promise, and it has a binding sending it somewhere, but why does the @
have to be there?
In most JavaScript implementations of Mustache, including Stache, the default behavior when encountering a function is to call the function to get its return value. However, here we need the rendering function to be a function, so we can use it later as a partial renderer. That @
indicates to Stache that, even though what's in value
is a function, it shouldn't be executed for its return value; rather, it should be the return value.
Now that we've established what's being sent out from the can-import
tag, let's look at where it goes. The Stache expression *example
is pretty basic; there are no path separators, and we can be reasonably certain that it's going to be stored in a property named *example
on the scope, but why that leading character?
That asterisk (*
) is a special marker for Scope lookup. Remember that a can-view-scope
is a stack of context objects that Stache looks at to find the values for different items. Normally, every context object on the Scope is a meaningful object outside of it. Maybe the current context is a data model, and quite likely it's a sealed DefineMap that can't accept new properties. But here in the Stache template we might need to push a new value into the scope from somewhere like a can-import
's view model, without disturbing any of the existing contexts. To solve this, can-view-scope
provides a reference scope, a special Scope that has its own private context, and any *
-prefixed key is assumed to be in it. So sending a value to *element
means that it gets put on the reference scope with the key *element
.
Finally, the >
operator in Stache magic tags renders partials, which are other renderer functions whose output gets placed in the parent template at the location where called. We read the partial from the reference scope, using the key *example
, and the Stache template that initially came from importing "example.stache" gets rendered with the current context. Easy, right?