Au 2.0RC: Problems with the new Enhance API where enhanced markup doesn't have access to the view-model where it is defined (worked in Au 1.0)

Hello,

DevExtreme widgets and the new Enhance API are working well together. But of all the scenarios I tested after having implemented my new bridge between Aurelia 2.0 and DevExtreme, and incorporating all of the knowledge I acquired on this forum, I did not test this one (apparently):

bid-jobsites-list.html (NOT WORKING)

<div class="tsi-list-layout">
   ...
   <tsi-dx-popup
     visible.two-way="showDescription"
     content-template="description"
     container=".tsi-list-layout"
   >
     <dx-template name="description">
       <my-description description.bind="someText"></my-description>
     </dx-template>
   </tsi-dx-popup>
   ...
</div>

where someText is on the view model associated with bid-jobsites-list.html. After the template is enhanced and ingested into the DevExtreme widget, it loses its context with respect to the view-model associated with the owning view (where it is defined). It cannot find someText.

A number of DevExtreme widgets do not pass in data to some of their templates. In that case, the widget has noModel set to true by the widget internally. Most of the widgets do pass in data, and I hand this off to enhance in the familiar way:

...
onResolve(
   Aurelia.enhance({
     container: config.container,
     host: $template,
     component: {
       data: renderData.model,
     },
   }),
   (enhancedView) => { ... }
...

All works well. But components like Popup, Popover, Tooltip, etc. have content templates where no data is passed in. Apparently, it is expected that the data will come from the template’s context, not from the widget.

Compared to Aurelia 1.0

This was no problem in Aurelia 1.0. Consider the following from one of my components in Aurelia 1.0:

tsi-model-editor-command-bar.html (WORKING)

...
<tsi-dx-popup visible.bind="confirmClose">
   <dx-template name="content">
      <div class="tsi-dialog__text">${text.areYouSure}</div>
      <div class="tsi-dialog__text">${text.closingWithoutSaving}</div>
      <div class="tsi-dialog__buttons-wrapper">
         <div class="tsi-dialog__buttons">
            <tsi-dx-button styling-mode="text" text.bind="text.keepEditing"
                  click.delegate="onCancelClose()">
            </tsi-dx-button>
            <tsi-dx-button type="default" text.bind="text.closeAnyways"
               click.delegate="onConfirmClose()">
            </tsi-dx-button>
         </div>
      </div>      
   </dx-template>
</tsi-dx-popup>
...

If you take a look in the dx-template markup above, you’ll see a reference to text.* in some of the bindings. That’s a field defined on the view-model. You can also see a number of delegate bindings. Their handlers are also on the view-model.

Context was preserved in Aurelia 1.0 and the text bindings and delegate bindings worked even after the enhanced template was ingested.

What’s going on here? Why are the dx-templates not seeing the view-model where they’re defined?

Maybe can you help with a simple repro? Itll be easier both ways: understanding and explaining the issues.

Hello @bigopon ,

Thank you for getting back to me.

I’ll need a bit of time to put the repro together. I have to figure out how much of the codebase to bring in to get to a minimum repro.

I’ll work on that tomorrow.

Hello @bigopon ,

I have posted a repro at…

I would draw your attention to welcome-page.html, at the bottom. I have a dx-template that I’ve defined as the popup’s content template. You’ll notice that I reference the view-model with this line:

<div>${description}</div>

But the description doesn’t show in the popup. This worked just fine in Aurelia 1.0, even after the template was ingested into the DevExtreme widget.

The DI container is configured in tsi-dx-widget-base.js:

this.scopedDiContainer = this.diContainer.controller.container.createChild({
      inheritParentResources: true,
});

All of my view-only wrappers inherit from this base class.

And the enhancement is defined in TsiDxAuTemplateBridgeService.js:

createRenderer(rawTemplate, renderData, config) {
    const $template = rawTemplate.cloneNode(true);
    renderData.container.append($template);

    onResolve(
      Aurelia.enhance({
        container: config.container,  // <-- The scoped container from tsi-dx-widget-base.js is supplied here
        host: $template,
        component: {
          data: renderData.model,
          contentData: this.contentData,
        },
      }),
      (enhancedView) => {
        dxEventOn($template, 'dxremove', () => {
          if (!enhancedView) {
            return;
          }

          onResolve(enhancedView.deactivate(enhancedView, config.controller), () => {
            enhancedView.dispose();
          });
        });

        return $template;
      },
    );
  }
1 Like

thanks @estaylorco , that’s quite a bit of a repro. It doesn’t seem to me you are doing anything special with the template compilation, all you did were compiling <dx-template>…</dx-template>, what about using a precompiled version of the content of your widgets instead?

During compilation, any content of a custom element is compiled into projections property of the element instruction, as if they are putting inside <template au-slot=”default”>.

So I would simplify your popup like this Aurelia 2.0RC: Enhance API and Context (npx makes) - StackBlitz

Btw good job working out the API of those widget. There’s no doc :smiley:

Hello @bigopon ,

I’m puzzled by your reply…but I’m also laughing a bit! I think you might need a break :joy: …or perhaps I do! I get the feeling that you don’t remember that we covered all of this ground! You and @Sayan751 were very helpful in helping me wrangle the new Enhance API (please see our other posts).

Everything was working fine until I hit content-only templates amongst the DevExtreme widgets, where data has to come from elsewhere, not from the widget itself.

Also, I would ask you to re-read the original post, especially the part where I compare to Aurelia 1.0. It’s really welcome-page.html that has the interesting bits…at the bottom.

I don’t know what you mean by this in your reply:

So I would simplify your popup like this Aurelia 2.0RC: Enhance API and Context (npx makes) - StackBlitz .

You’ve provided a link to my own repro! I’m the one who wrote the bridge for Aurelia 2.0/DevExtreme template integration.

Also, you said this, which is confusing me (although thank you for the kudos, of course):

Btw good job working out the API of those widget. There’s no doc :smiley:

If you’re referring to the DevExtreme widgets, they are extremely well documented. So is their integration story.

So your reply doesn’t actually answer my question. Do you need me to clarify what it was I was asking?

That’s an oop, your stackblitz link has embed=1 in it, which hid the fact that I wasn’t signed in, so i kept saving not knowing that it will not matter.

Recreated the demo https://stackblitz.com/edit/stackblitz-starters-u49gj5xh?file=my-app%2Fsrc%2Fdx-popup.js

If you’re referring to the DevExtreme widgets, they are extremely well documented. So is their integration story.

I couldn’t find doc specifically for the integrationOptions property in the options object, had to guess quite hard :smiley:

I’m puzzled by your reply…but I’m also laughing a bit! I think you might need a break :joy: …or perhaps I do!

:grin:

Hello @bigopon ,

…which hid the fact that I wasn’t signed in…

Ah, that explains it!

I see what you mean about integrationOptions. Yes, it’s covered elsewhere in their documentation. It’s not considered a mainstream API, so you won’t see it amongst the components. They cover it here: Advanced Configuration.

I took a look at your repro. I noticed that you’re simply using their widget directly. I know that under those circumstances, it works. I need it to work with my wrappers (the wrappers work perfectly in every case where data is being pushed into the Aurelia-enhanced template from the widget itself). The whole of this machinery fails only with noModel templates: widget templates that are content only, no data. The widget sets noModel=true when any given template has no data from the widget associated with it, only content.

When I change the welcome page markup to this (using my wrapper instead):

<tsi-dx-popup
    visible.two-way="showDescription"
    defer-rendering.bind="true"
    content-template="description"
  >
    <dx-template name="description">
      <input value.bind="message">
      <div>${description}</div>
      <div>${message}</div>
      <hr>
      <tsi-dx-button text='Close' click.trigger="showDescription = false">   </tsi-dx-button>
    </dx-template>
</tsi-dx-popup>

it doesn’t work.

But it worked in Aurelia 1.0: in 1.0, ${description} and ${message}, to take from the example above, could sit comfortably on the view model of the owning component, where the dx-template is defined in markup.

Please take a look at tsi-dx-widget-base.js. I configured the scoped container based on your advice (which has been working famously well). You’ll see that this DI container flows through to the #enhance call in the #createRenderer method of TsiDxAuTemplateBridgeService.js.

What happened between 1.0 and 2.0? Why do we lose context in 2.0?

with this particular code

      Aurelia.enhance({
        container: config.container,
        host: $template,
        component: {
          data: renderData.model,
          contentData: this.contentData,
        },
      }),

You are not having anyway to bind the scope of WelcomePage to the rendered template. I’ve adjusted the bridge code like this


    // Necessary to avoid a hydration instruction mismatch
    const $template = rawTemplate.cloneNode(true);
    // Place the template into the container the Dx widget provides
-   // renderData.container.append($template);

+   const view = Controller.$view(
+     this.rendering.getViewFactory(
+       {
+         name: CustomElement.generateName(),
+         template: rawTemplate,
+       },
+       config.container
+     )
+   );
+   view.setHost(renderData.container);
    // This is the meat of creating a renderer
    // Will automatically pick up the view resources of the consuming component as we will configure the container with those resources
    // DevExpress chose 'data' as the property on which all of the underlying component's relevant data is stored,
    // but it could be called anything; the component on the enhance config is the component representing the template,
    // not the consuming component
    onResolve(
+     view.activate(view, null, config.controller.scope.parent),
-     // Aurelia.enhance({
-     //   container: config.container,
-     //   host: $template,
-     //   component: {
-     //     data: renderData.model,
-     //     contentData: this.contentData,
-     //   },
-     // }),

so you have a chance to connect the scope properly.

Hello @bigopon,

OK, so I tried your modifications, which make sense but cause a ripple effect that brings down all of the data-driven templates of the widgets (the content-driven templates, as with the popup widget, are working now).

I can see why that is. In your code in green, you have this:

...
{
   name: CustomElement.generateName(),
   template: rawTemplate,  // <-- problematic
},
...

In order for this to work with the data-driven templates, the node has to be cloned:

// Necessary to avoid a hydration instruction mismatch
const $template = rawTemplate.cloneNode(true);

...
{
   name: CustomElement.generateName(),
   template: $template, // <-- Solves the instruction mismatch problem
},
...

But where does renderData.model go? This is the data that the DevExtreme widgets push out in the #createRenderer call. Because this is missing, none of the widgets enjoy access to the widget’s data (but all other errors go away). I would think that on #getViewFactory, I could supply a bindingContext that might look this:

{
   data: renderData.model,
}

which is what I do in the Aurelia 1.0 bridge code.

You see, there are some components that have both data-driven templates and content-driven templates. So I can’t lose the data pathway.

is there any code that can show where data comes from, or how to test it in the template?

Regardless, I’ve added some code to bridge in the data.

    onResolve(
-     view.activate(view, null, config.controller.scope.parent),
+     view.activate(
+       view,
+       null,
+       Scope.fromParent(config.controller.scope.parent, {
+         data: renderData.model,
+         contentData: this.contentData,
+       })
+     ),

Hello @bigopon ,

That’s it! Both the content–only pathway and the data pathway are working…and no memory leaks!

Man, I love Aurelia! :heart:

With this fix, I was able to eliminate this:

{
   ...
   contentData: this.contentData,
   ...
}

since this data is now coming directly from the view-model (as it did in Aurelia 1.0). To satisfy the data pathway (not the content-only pathway), this code is still necessary:

// Necessary to avoid a hydration instruction mismatch
const $template = rawTemplate.cloneNode(true);

...
{
   name: CustomElement.generateName(),
   template: $template, // <-- Solves the instruction mismatch problem
},
...

Simply specifying rawTemplate causes an instruction count mismatch error. I have to clone the node. While Aurelia 1.0 didn’t produce the error by that name, it was necessary there as well to clone the node.

Thank you so much for your help!

2 Likes