Trouble migrating from Au1 to Au2 where certain technical APIs are being utilized

Hello,

I am continuing the journey of migrating from Au1 to Au2, and have reached the point of integrating Devextreme (from DevExpress), which I will abbreviate as Dx.

BACKGROUND

In our Au1 application, I have successfully integrated Dx, even to the extent of marrying Aurelia’s templating to Dx’s templating through the integrationOptions property on those Dx components which support dx-templates.

It would appear that a number of API’s have moved, changed, or been eliminated. Consider the imports from my DxAuTemplateService:

import { inject, TemplatingEngine, View, ViewResources } from 'aurelia-framework';
import { createOverrideContext } from 'aurelia-binding';

Of course, I now import inject from ‘aurelia’.

I know from the documentation that TemplatingEngine is now under the Enhance API in Au2. And that’s fine. I just have to change the method call to reflect the new signature (I’ll work on that later). From our Au1 application, I have:

// Here, we build an Aurelia view from all of the moving parts
const view = this.templatingEngine.enhance({
    element: element,
    bindingContext: itemBindingContext,
    overrideContext: itemOverrideContext,
    resources: viewResources,
});

Finally, it would appear that @noView and @processContent have changed as well. I haven’t been able to migrate the following use case (i.e., the specific combination of the two decorators below):

@noView()
@processContent(false)
export class TsiDxButton() {
    ...
}

I learned the above approach from the two Vimeo training courses offered by Rob Eisenberg himself a number of years ago (see Question #3 below for a fuller discussion).

Also, know that I have successfully wrapped DxButton from Dx, but have not yet been able to bring Aurelia’s templates together with Dx’s. That’s what the questions below address.

QUESTIONS

1. What happened to #createOverrideContext?

import { createOverrideContext } from 'aurelia-binding';

I use it in the following way (first branch of the if below):

...

if (model) {
    itemBindingContext = {
        data: model, // Push an Aurelia model into the data property
    };

    // Not strictly necessary, but assists Aurelia with scope hierarchy traversal
    itemOverrideContext = createOverrideContext(
        scope.bindingContext,
        scope.overrideContext
    );
} else {
    // Here, we just reuse the parent scope; not all dx-templates are associated with
    // data (a model)
    itemBindingContext = scope.bindingContext;
    itemOverrideContext = scope.overrideContext;
}

const view = this._createAureliaViewFromTemplate(
    newElement,
    itemBindingContext,
    itemOverrideContext,
    scope.owningView.resources
);

...

2. What happened to ViewResources?

I know that View is now EnhancedView. But what happened to ViewResources? I use them only in contract language to make sure I’m working with instances of these classes. So they aren’t strictly necessary for the template service to work. But I place many contracts around third-party API usages in order to detect intimate changes in those APIs:

...

require: {
    element, 'Element exists';
    itemBindingContext, 'Item binding context exists';
    itemOverrideContext, 'Item override context exists';
    viewResources instanceof ViewResources,
        'View resources are of type ViewResources (Aurelia)';
}

...

return view;

ensure: {
    result, 'Aurelia view exists';
    result instanceof View, 'View is of type View (Aurelia)';  
    // I know that View will become EnhancedView
}
    

3. How do we migrate the following decorator chord?:

@noView()
@processContent(false)

With Dx components, @noView allows the component to take over the view. I have determined that the new pattern for that in Au2 is the following:

@customElement({
    name: 'dx-button',
    template: null,
)

where template: null is the equivalent of @noView.

@processContent is a bit trickier. Consider the following two snippets from our Au1 application:

@noView
@processContent(false)
@inject(Element, DxAuIntegrationService, CustomEventDispatcher)
export class DxSelectBox extends DxTextualInputBase {
...
}
<dx-select-box items.bind="typeOptions" value-expr="id"...>
    <dx-template name="item">
        <div innerhtml.bind="data.type"></div>
    </dx-template>
    <dx-template name="field">
        <dx-text-box is-static.bind="true" value.bind="data.typeUnformatted">
        </dx-text-box>
    </dx-template>
</dx-select-box>

It would appear that Au2 strips out anything between the custom element’s tags. The way my template services works, I harvest the DX-TEMPLATE elements, ingest them into the template dictionary, and then pass that dictionary to the Dx component’s integrationOptions, as specified by the DevExpress team. After the ingest, I delete the DX-TEMPLATE elements as they are no longer needed.

@processContent(false) simultaneously allows me to have content but suppress it in the view (because it’s going to be processed by me and removed).

How do achieve this scenario in Au2? This was explicitly covered in Rob’s course, and I use it in our Au1 application with great success!

1 Like
  1. For @noView:

    Finally, it would appear that @noView and @processContent have changed as well. I haven’t been able to migrate the following use case (i.e., the specific combination of the two decorators below):

    @noView()
    @processContent(false)
    export class TsiDxButton() {
        ...
    }
    

    As you noted, @noView can now be achieved with template: null in the custom element options, though it’s a good thing that we can create a decorator @noView to specify that.


  1. For @processContent(false):
    Thanks for explaining the intention. It can be achieved in v2 via the data property on the instruction of the custom element instance
    import { resolve, IInstruction } from 'aurelia';
    import { HydrateElementInstruction } from '@aurelia/template-compiler';
    
    @processContent((node, platform, instructionData) => {
      instructionData.template = node.querySelector('dx-template');
    })
    export class DxSelectBox {
      constructor() {
        const { data } = resolve(IInstruction) as HydrateElementInstruction;
        const template = data.template;
        // do your stuff here
      }  
    }
    
    As you noted you’ll delete the <dx-template> after the extraction during the compilation, I think the above should suffice.

  1. For the post itself:

    … How do achieve this scenario in Au2? This was explicitly covered in Rob’s course, and I use it in our Au1 application with great success!

    Thanks for the long detailed post, (and others). It may give some more good idea where in the doc we can improve, maybe a course is needed again. cc @dwaynecharrington .

1 Like
  1. For the question 1:

    What happened to #createOverrideContext?

    the option to provide a completely different object to be used as override context is no longer there, but you can always add more properties to the override context created before bindings are bound. The good phase for that would be created lifecycle. That said, I think it maybe cleaner to allow a custom overridecontext without having to poke/hook into the internal of the composed element. Maybe can you help create an GH request? cc @Sayan751 @fkleuver

  2. For the question 2:

    What happened to ViewResources?

    It’s now the container itself holding the registration of resources, so you can say it’s consolidated. I’m not sure how to guess your resource APIs declaration & validation, maybe you can give some examples so we have a better idea?

Thanks for your insight on this!

I do see a rub, though, with the following fragment:

instructionData.template = node.querySelector('dx-template');

Often times, there is more than one dx-template in the markup for a particular Dx component (especially true of that gargantuan DxGrid).

But now, as I write this, I just remembered that templates in Au2 can be multi-rooted. We no longer have to bracket the template with <template>. So, then, perhaps #getElementsByTagName will give me access to the full set.

I’ll work on this in a bit (you’ve given me a lot of insight to digest).

I can see now that I need to work towards full understanding of the migration to the new API’s, at which point I will be sure to post here for everyone’s consideration and contemplation.

If we had:

<dx-select-box>
  <dx-template name="item">...</dx-template>
  <dx-template name="field">...</dx-template>
  <dx-template name="justInCase">...</dx-template>
</dx-select-box>
<dx-select-box>
  <dx-template name="item_in_another_usage">...</dx-template>

You can easily get any information you like, like this

@processContent((node, _, data) => {
  data.itemTemplate = node.querySelector('dx-template[name="item"]');
  data.fieldTemplate = node.querySelector('dx-template[name="field"]');
  data.justInCseTemplate = node.querySelector('dx-template[name="justInCase"]');
  data.item_in_another_usage = node.querySelector('dx-template[name="item_in_another_usage"]');
})

and so on.

Note that in the 2nd usage of <dx-select>, the first three querySelector calls all return null, while in the first <dx-select> usage, the last querySelector call returns null.

The example was to emphasize that there’s not been really any change in the APIs, except that we now have an official way to associate the element usage in the template with the instruction instance, which is injectable in element constructors.

I just gave this a try (from your first example), I since I’m not using TypeScript, I had to translate. I think I’m missing something, though. I did this:

...

import { HydrateElementInstruction } from '@aurelia/template-compiler';
import { DxBase } from './dx-base';

import Button from 'devextreme/ui/button';

@inject(Element, CustomEventDispatcher, HydrateElementInstruction)
@customElement({
    name: 'dx-button',
    template: null,
    processContent: (node, platform, instructionData) => {
        instructionData.template = node.querySelector('dx-template');
    },
})
export class DxButton extends DxBase {
    ...
    constructor(element, eventDispatcher, instruction) {
        super(element, eventDispatcher);

        console.log(instruction);  // <-- I do get an instruction, but I'm skeptical

        const { data } = instruction;  // <-- This is where it fails
        const template = data.template;
    }
}

Should I be accessing the container directly to resolve HydrateElementInstruction?

I have to start winding down this evening. Tomorrow’s our first full day back from the holiday.

I see I have a number of replies to make:

  • I’ll provide as a migration specimen the full template service that I wrote for processing dx-templates into Dx components as Aurelia enhancements (it’s not terribly complex).
  • Initiate/facilitate a GH request around #createOverrideContext, or a least its equivalent cc @Sayan751 @fkleuver .
  • Get back to you with the makes one-liners you requested.

Concerning @noView, it would indeed add semantic clarity. But, honestly, template: null doesn’t require a huge leap of assumption either. Perhaps, to protect developers, it should have to be null specifically. Maybe undefined and false should throw an exception?

It should be

@inject(Element, CustomEventDispatcher, IInstruction)

The interface is IInstruction and the instrance is actually a HydrateElementInstruction.

If you just inject HydrateElementInstruction, you’ll get an irrelevant instance, constructed anew.

It’s for the next time you report a scaffolding issue. For the css issue, it’s resolved fix: fix webpack config on css by 3cp · Pull Request #116 · aurelia/new · GitHub

Concerning @noView , it would indeed add semantic clarity. But, honestly, template: null doesn’t require a huge leap of assumption either. Perhaps, to protect developers, it should have to be null specifically. Maybe undefined and false should throw an exception?

undefined is considered nullish, just like null so it’ll likely be fine. false is an interesting value, maybe we can throw if neither nullish or string to guard the APIs, in case of future expansion. For @noView, it may help ease the unnecessary manual work during migration, not a bad thing, maybe I’ll cc @Sayan751 since he also did it.

Created this GH issue: Implement @noView and @inlineView like au1 · Issue #2085 · aurelia/aurelia · GitHub

1 Like

Thank you for opening that issue.

This evening, I’m going to finish implementing—or at least try to—guidance that @bigopon gave me in dealing with the absence of @processContent(false). Unless anyone objects, I might open an issue recommending @processContent(false). I bring it up here because it goes hand-in-hand with @noView in those cases where a third-party component takes over the view, but there’s still a need to process markup into, or against, the component’s instance.

I say “might” above because it’s going to depend on the shape of the code necessary to deal with the absence of @processContent(false).

I have created a GH request (Port #createOverrideContext (from Au1 'aurelia-binding') to Au2 · Issue #2086 · aurelia/aurelia · GitHub) for createOverrideContext.