ES6 module pre-injection

I think I’m missing a key concept.

I created a wrapper for aurelia-dialog, that gets the DialogService injected.
I’m also utilizing (as bad practice and as temp scaffolding) a “managers object” to pass around that should contain a reference to my DialogMgr
link@line 19
However, the DialogMgr property after page load, only contains the reference to the constructor, not the post-injection singleton that aurelia normally creates.

I can, in my app.js after loading, reassign the property through that dependency injection. I’m curious what I’m missing that prevents my const mgrs from having the instantiated object.

@Kremnari Why not convert the mgrs object to a class and inject the DialogMgr instance via constructor? That way you can also inject the singleton mgrs in your other classes.

However, the bigger question IMO is why you need such a pattern. Why not directly injection the instances to the classes as per the need? Why do you need this wrapper object?

I was initially passing around the individual mgrs to constructors but that seemed both tedious and unnecessary with dependency injection. I didn’t form it as it’s own class as I didn’t need it as anything other than an object literal (container). As it exists, I don’t even need to inject it, I just import it into global scope.

The wrapper object for dialog allows me to use it like
await mgrs.DS.open("SelectBus", {base: mgrs.baseApp}),
IE await simplicity… just looking at it, I should be able to simplify it further to something like
await mgrs.DS.SelectBus(paramObj)

My code style is still developing :smiley: While there is some guidance for game patterns, there’s still a lot of grey areas that depend largely on what is being developed, and the mechanics I wanted were merely broad-stroked in the beginning.

But more to the point, I’m curious about the instantiation mechanics at play. In the object literal, I cannot assign the module export, nor use new and get a class instance with the injected dependency. How, and when, might the inject be executing to create the au’s DI version of the class?

IMO injecting the instances vis ctor makes it easier to test the components. Testing with your pattern might get tricky.

If testing is not a concern for you then, you can create your mgrs object literal after aurelia is started. For that you can do something like this.

// this goes to your main.js 
mgrs = {
//...
DS: Container.instance.get(DialogMgr)
//...
}

This is effectively what I’m doing, just adding that property in my main.js.

Your statement of “after aurelia is started,” is kind of the key I’m trying to understand. The inject process is dependent on aurelia being ‘started;’ where are the details of that lifecycle? When does inject actually do it’s processing?

That actually is dependent on how DI is instantiating the components. @inject is just working as the metadata provider (in this case the dependency information provider). In short, the components instantiated as per dependency graph (from view and view-model), and during instantiation, the DI injects the deps to class ctor, as per the info provided by @inject/@autoinject. This is ofc a simplified version.

Now the reason, I mentioned to do this “after aurelia is started”, is because, we want all the initializations work from every plugin to be completed, before you request for an instance to the DI. Otherwise you may end up with an “invalid” instance of the class. With Aurelia1 the DI and the Contracts are based on abstract classes. So until everything is properly initialized (which is surely done after aurelia.start() is resolved), asking the Container for an instance of class may result error, as that might try return an instance of the abstract class, which is certainly not intended.

Hope this makes sense.

I’m not sure what your needs are, but I created a wrapper class around aurelia-dialog for my needs. It lets me do things like this. I built in a good bit of options that work in my application like auto close, the ability to change the footer if needed on the custom dialogs. Aurelia dialog was a great starting point, but I’ve often considered scrapping it in favor of a full custom implementation.

alerts

let alert1 = this.dialogFactory.alert("Alert 1 title", "alert content", { allowEscapeToDismiss: true });

confirm

 let result = await this.dialogFactory.confirm("Simple confirm", "It doesn't get any easier than this");

custom view models

let settings: CustomSettings<CustomDemoModel> = {
      viewModel: CustomBody,
      model: { name: "Joe Smith" }
    };

let result = await this.dialogFactory.custom<CustomDemoModel, CustomDemoResult>("My custom", settings);

The custom view and view Model for the above


import { CustomDemoResult } from "./customDemoResult";
import { DialogContainerBase, DialogOkArgs } from "dialog-factory";

export class CustomBody extends DialogContainerBase<CustomDemoModel> {
  public async ok(args: DialogOkArgs) {
    let result = new CustomDemoResult();
    result.newName = this.model.name;
    await this.doWork();
    return result;
  }

  doWork(): Promise<void> {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        resolve();
      }, 50);
    });
  }
}

export class CustomDemoModel {
  public name: string = null;
}
<template>
    <dialog-container>
        <div slot="body">
            <div class="form-group">
                <label for="nameField">Name</label>
                <input class="form-control" value.bind="model.name" type="text" id="nameField" />
            </div>
        </div>
    </dialog-container>
</template>
2 Likes

I’ve been working on my code, and finally remember the right response to this. The reason my mgrs object isn’t a class, is so I can just import it into the relevant files. As a class (or au-strapped class) it would need to be instantiated and/or injected to be used. As it exists, I can use it freely by reference mgrs.yadda.yadda without having to store it. This improves the visibility of the object as it doesn’t require a this.

I understand your reasoning regarding putting your mgrs object in the global scope. At first glance, making it generally available in your components that way might indeed seem to be easy and straightforward.

However, Sayan751 made an interesting point:

IMO injecting the instances vis ctor makes it easier to test the components. Testing with your pattern might get tricky.

Suppose you want to create automated tests (like unit tests or so) to make sure that the internal logic of your components will not break when you start adding features or fix some bugs. If your components are secretly dependent on objects in the global scope, like your global mgrs object, creating tests will become very tricky, since those tests will probably need to address such global objects as well. When you inject external dependencies (like your mgrs object) via the constructor, your components’ external dependencies are not only more transparent, but also much easier to replace with a dummy/mock object that is suitable for your automated tests at hand.

Testing is just one example. In general, you actually don’t want dependencies you can’t control very well during runtime (like dependencies on objects in the global scope). Those might introduce unclear and unexpected side effects in the long term and make debugging a nightmare. And avoiding global objects will also keep the global space clean, which is always a good thing. What would happen if certain global objects required by component A would conflict with certain global objects required by component B? Or even worse: with certain global objects that are internally created by the Aurelia framework? (I don’t know if there are any important global objects created by the Aurelia framework, but I really don’t want to find out the hard way…)

In short: using dependency injection (and avoiding global objects) will promote loose coupling between your classes/components and ease testing, thus resulting in a more robust and more flexible codebase.

1 Like

Well explained! The mgrs in this case however is still exposed via a JS module, so it might not pollute the “global” space in “true” sense, but all other arguments related to testing, and coupling still holds.

Especially for testing, you don’t want to share a “global” object across all your tests, as that might potentially result in residual state from previous test. And it’s very very scary, when the tests starts failing randomly!!