aurelia / templating

An extensible HTML templating engine supporting databinding, custom elements, attached behaviors and more.
MIT License
116 stars 103 forks source link

Question: how can a processContent( ) functor inject resources prior to calling compile? #329

Closed kevmeister68 closed 8 years ago

kevmeister68 commented 8 years ago

I have a processContent decorator attached to my custom element, and in it I basically consume some of the content provided to my custom element and then use the compiler to compile a new piece of HTML.

In the new piece of HTML I constructed, I tried putting tags but it doesn't work. I have read https://github.com/aurelia/templating/issues/245 (and I have used that technique elsewhere with enhance), but for processContent that's not a workable solution.

Is there any way I can inject into the ViewResources object passed to my functor so my additional components are "known about" ?

EisenbergEffect commented 8 years ago

@kevmeister68 The processContent function looks like this processContent(compiler, resources, node, instruction) The second argument is an instance of ViewResources: http://aurelia.io/docs.html#/aurelia/templating/1.0.0-beta.1.2.2/doc/api/class/ViewResources

You can technically push additional elements (or anything) into that and it will affect the view that is being compiled. You would need to have fully loaded and initialized instances of HtmlBehaviorResource to add though. If you can provide some more details, I may be able to help you further.

kevmeister68 commented 8 years ago

@EisenbergEffect Thanks for the offer of assistance.

Basically I am building a tab-control widget. The underlying mechanics are based [simply] on bootstrap CSS at present. My HTML pattern for describing the tabs was "inspired" by seeing how an aurelia-dialog was defined. I wanted my tab definition to look like this:

<my-tabcontrol>
   <tab-page>
      <tab-heading>
         Customers
      </tab-heading>
      <tab-content>
          <some-datagrid-thingy></some-datagrid-thingy>
          etc
      </tab-content>
   </tab-page>
</my-tabcontrol>

Now, my understanding of what processContent( ) could do was that it would permit me to manipulate the DOM (as passed in through the third parameter) prior to Aurelia enhancing it.

Internally I manipulated the DOM to rearrange the above structure to something like:

<my-tabcontrol>
    <ul class="nav nav-tabs"> <!-- bootstrap stuff -->
        <li><a>Customers</a></li>
    </ul>
    <my-tabpage active.bind="someExpr" activate.bind="anotherExpr">
        <some-datagrid-thingy></some-datagrid-thingy> <!-- from above -->
    </my-tabpage>
</my-tabcontrol>

In the above example, <my-tabpage> is a custom element driven by a HTML file only (ie. no view-model). It exists to simply wrap its content in a couple of <div> elements to control which "page body" of the tab is visible.

I ran into several problems (acknowledging that I tried several variations to make this work):

Where compiler, resources, and instruction are parameters 1, 2, and 4 passed into the processContent functor.

I tried the above approach (ie. instead of manipulating the DOM, I used the passed-in node to formulate a new piece of HTML and compiled it) and it worked (I then deleted the "source" DOM content), but since my <my-tabpage> element is a "custom element" without a view-model, it was not getting translated.

(It worked, but I don't even know what a viewFactory "is", and what it "means" insofar as what processContent is doing, etc).

I tried adding a <require> element into my generated HTML but the compiler.compile call does not [appear to] honor the require element.

My present workaround (as much for learning as anything else) is to inject a global resource for "my-tabpage" in my global app file, and an associated "resource rename" (cause my element name does not match my filename), but I find this really ugly.

So.... this brings me to the question raised. Can my processContent( ) method somehow manipulate the viewResources object to add the necessary reference for "my-tabpage".

I understand the principles behind perhaps using viewResources.registerElement( ) to somehow register a new custom element, but I have no idea of the objects involved and their initialisation, not to mention the fact that any "loading" (of a template for example) will be an async operation so I have no idea how that can happen within processContent anyway.

I am finding the code somewhat impenetrable in terms of attempting to reverse-engineer a solution from the four parameters passed in. The three Aurelia objects passed in have a significant surface area to try and reverse-engineer a solution from, and even if I did, I don't really know what's "acceptable" to mess around with or not.

I'd love to write up some of this stuff for others, but I'd really like to be confident in my understanding of what's going on to feel I can make an adequate contribution.

Somewhere or other, I'd like to start a document describing the Aurelia internals and the terminology involved (ViewCompiler, ViewFactory, ViewSlot, TemplateRegistryEntry, TemplateDependency, "behaviour", "instruction", etc). I am happy to do that, once I learn what they are :-)

EisenbergEffect commented 8 years ago

FYI I am not ignoring you. I've been super busy :) I'd like to get you a more detailed response but...

First, the processContent should work. You can't introduce new elements into the view resources at that time. Those need to be present already. But, the general hook should allow you to transform one set up markup into another. Is that not working? Or is it just not finding the new custom element? If that's the case, consider adding that custom element as a global resource.

Let me know.

kevmeister68 commented 8 years ago

No worries, I don't think you're ignoring me. I realise that you've got heaps of stuff on. For me a number of questions have arisen:

Beyond the above, which relate to my actual problem, I'd like to get an understanding of the various terminology used within the Aurelia objects, as mentioned above (ViewSlot, Behaviour, Instruction, etc).

Thanks

EisenbergEffect commented 8 years ago

Technically, you could add it to the view resources inside the processContent, but, as you observed, there's an async process by which resources are loaded and made ready to use. So, that would need to be done earlier. If you wanted to get tricky, you could register it as a global resource, then remove it from the global resources, then take the instance and add it to whatever resources you need. That should work just fine. There isn't an api explicitly for removing things. But, if you take a look at the ViewResources in the templating repo, I get you can figure out how to make it happen. See if that helps get you a bit closer to what you want.

ViewSlot - Represents a location within the DOM that Views can be added/removed from. This abstracts the animation system and the view lifecycle so that it all "just works".

Instruction - These are the internal representations that the compiler stores which it uses to later instantiate views with all their behaviors.

Behavior - Typically refers to either a custom element or a custom attribute.

ViewCompiler - Takes a template and ViewResources as input and returns a ViewFactory.

ViewFactory - Encapsulates all instructions for creating a view instance and has the ability to create a View on demand, including the ability to cache and re-use View instances.

View - The actual runtime representation of a View with all its Bindings, Behaviors and child Views.

TemplateRegistryEntry - Stores the HTML template, along with it's dependency list. This is also used to cache the ViewFactory for the template.

TemplateDependency - Something that a template requires in order to be properly compiled by the ViewCompiler.

kevmeister68 commented 8 years ago

So there is nothing in the custom-element life-cycle model that would enable me to register the custom-element earlier, and not use Global Resources at all ?

Actually, I've just had a brilliant idea (well, maybe). In a similar vein to @inlineView, would it be feasible to build a decorator @inlineResource or @inlineRequire that could identify the resource that needs to be "required" into a custom-element? (Of course, on the presumption that it then be available when the processContent( ) method invokes the view-compiler).

EisenbergEffect commented 8 years ago

The two direct paths to making a custom element available are to either make it global or to require it in the particular view that needs it. There are "other" ways you could find to make it happen. I'll need to think about what the best approach to recommend to you is. Perhaps we need some general feature that says "whenever resource X is required, be sure to also make resource Y available"? That could be a nice way to handle the common scenario with more complex elements that have special child element and remove the pain of having to explicitly require everything. Requiring the parent could just bring in the children maybe. Thoughts?

kevmeister68 commented 8 years ago

I guess the first comment I would make is that I'm not likely to go digging around to find "other ways" to achieve the outcome, unless you "advocate" them. One of the problems with JS - nothing to do with Aurelia - is that everything tends to look like a nail, giving you the impression that everything and anything is open-slather in terms of access and manipulation, yet the software engineer in me tells me that it is better to respect the integrity of what is meant to be "internal" and what is meant to be public. Hence I tend to be conservative in that regard. Besides, "jerry-rigging" a solution may not hold up well to future improvements/changes within Aurelia, whereas an "advocated" approach should be more immune to future disturbance.

(As an illustration of the above principle, I've written code that trawled the dom for "au" elements [or was it properties], later finding they had changed name to "aurelia". I have written code that picks out pieces from the aurelia data properties to obtain eg. to find the current View-Model object, only to find those structures changed in a later release. I knew when I wrote them that I'd get bitten, and so I did. Basically I was sticking my nose into places where I wasn't meant to be sticking it, but the JS/DOM have no means of telling you that).

And before answering your suggestion, I just want to be clear about why I consider using global resources "ugly". It's because the custom element cannot just be "used" as-is, it needs "configuration" (through registering its required global resources in order for it to work). That's an acceptable requirement for a plugin, because you are plugging in additional functionality. But for my example, the act of using a global resource is simply because there is no other way to inject the dependency when using processContent. So it's a kludge. In other words, processContent( ) in combination with using custom elements in the "processed" markup, is limited by the ability to specify "required" components.

To answer your question, my suggestion of an @inlineResource (or whatever name) decorator to specify a required dependency is one means of specifying what you have suggested. If the referenced custom-element has its own @inlineResource, an obvious cascade can occur (recursively). The potential obviously exists here for circularity (a requires b, b requires a) and that would have to be managed in whatever fashion made sense.

Whilst not specific to this issue, but it bears upon this issue, is that there is a lack of orthogonality (by my perception) when we get closer towards doing "more advanced stuff".

There's an absolute bucket-load of stuff I don't know about Aurelia (and wish I did), but I do seem to end up in some interesting situations (I'm not sure I'd call it "pushing the envelope", but let's think of it that way) that often show up this lack of orthogonality. I will list a number of them, and hopefully I can connect a thread amongst them:

Anyway, the thread I am drawing here is that there are corner cases that I would expect to offer orthogonal behaviour that presently isn't orthogonal. It doesn't affect many people because most people aren't trying to push Aurelia like I am (and I consider myself "pushing lightly").

So, back to your question, is defining a set of dependencies a good idea?

For a regular situation (standard custom element, HTML template file), I'd be pretty sure my answer is no. It does not achieve anything, and is unnecessary.

For the processContent( ) case, it feels like the fundamental problem is that I can't pass the dependencies directly to the viewcompiler, either as markup or through a DocumentFragment. Put another way, processContent( ) can't do what is achievable with getViewStrategy( ). The viewcompiler could never achieve its compile outcome with dependencies unless it became an async operation (ie. returns a Promise), and processContent( ) could never use it effectively unless it too became an async request.

My personal preference for a solution would be a combination of numerous things:

That means the client can use <require> tags if that works for them, or it can eschew that and inject dependencies directly into the ViewResources if that works better for it, etc.

A key point I would make is that it is not always possible to know in advance what dependencies you are going to need, other than the cop-out approach of defining a maximal set of every potential dependency (ie. your app won't magically bundle in new components, so you just list every component in your app as a possible dependency). Oooosh. This of course is a crappy outcome because you are potentially going to waste time loading dependencies that are not actually used.

So for that reason I would say that defining dependencies "in advance" or in a static fashion, is counter-productive to uses of processContent( ) that attempt to perform dynamic generation, but is "OK" for non-dynamic cases.

EisenbergEffect commented 8 years ago

Just to be clear, processContent and viewStrategy are completely orthogonal. viewStrategy is capable of creating a factory which produces the view for a component. This is the view that is rendered into shadow dom (or emulated shadow dom). processContent is a compilation hook for a component that runs when the element is used and allows the content placed inside the tag to be processed. View !== Content. The result of the content is then projected into the view's content slot (per shadow dom). That is to say that there are two different pieces of HTML that are composed according to a set of rules defined by the shadow dom spec. viewStrategy deals with the HTML that is part of the component itself while processContent deals with the HTML that is provided by the consumer on a per-use basis.

It is possible to do what you want, but we've intentionally made it more difficult because it wouldn't be considered a common, or even a good practice to do what you are trying to do. Introducing new variables into a larger scope mid-stream could produce highly unpredictable or erratic behavior. For example, consider that your component wants to dynamically introduce other-component during the process content phase. But, what if the consumer of your component has also created and required something called other-component? If it's locally required, then using your component in that situation will cause an exception to be thrown because Aurelia guards against two elements with the same name. So, the end user of your component will get an error, but it won't make sense to them because they can only see the one component that they have intentionally added. Another scenario is that the end user could have registered their own other-component as a global resource. In this case, when you try to add your own version there won't be an error, because it's possible to have local variables that hide global ones, just like in JS itself. However, once your component's content is processed, the resource will be overriden, which means that after that point in the template, the other-component that the end user is trying to use will be replaced by your version, thus creating unexpected and very hard to track down behavior for the consumer.

If you were going to do this at all, by dynamically adding things, you would need to generate random, unique element names to make sure you didn't create conflicts either through errors or overriding behaviors. You also need to load the extra elements before the view is compiled. You can do that using apis on the ViewEngine class. It can be done as part of a custom ViewStrategy. Note that the loadViewFactory method of a ViewStrategy returns a Promise, which means you can do whatever async loading stuff you want before handing off to the compiler to actually compile your view. You can create a custom strategy that just uses one of our built-ins but just decorates with additional loading behavior.

There are lots of ways to do dynamic things in Aurelia. You can use the compiler directly, the composition engine, view strategies, content processing. The most common way is to use the compose element, or to create your own version of the compose element that does dynamic composition the way you want to do it.

If you want a cheap way to have the require of one element to bring in another, simply export both of them from the same module. Requiring a module brings all of its exports into the view that it's imported into. If you do this, you are breaking with the convention of one module to one element, so you need to use the customElement decorator and a view strategy explicitly on each of those exports.