How to use Event Aggregator as Transient with Custom Element?

So I have this issue with my lib Aurelia-Slickgrid which is a Custom Element and I use the Event Aggregator in a few areas of the plugin but then I had some issues where certain actions were reflected in all elements created.

To give a concrete example of my issues, I create 2 or 3 grids (that is my custom element lib) and in the plugin I have some events which basically say something like when you get a filter cleared event then do other things in the grid and some of these actions were reflected in all my grids. At first I thought I had issues with DI and Services not being transient, I didn’t even think about the Event Aggregator, then after 2 days I found that the problem originate fromr the Event Aggregator being shared in the entire app. I tried setting the Event Aggregator as Transient with aurelia.container.registerTransient(EventAggregator); but that has no effect.

I saw this Event Aggregator doc which says

If you include the aurelia-event-aggregator plugin using “basicConfiguration” or “standardConfiguration” then the singleton EventAggregator’s API will be also present on the Aurelia object. You can also create additional instances of the EventAggregator, if needed, and “merge” them into any object.

It basically mentions about the Singleton, but doesn’t seem to say what to do for my use case (I want transient Event Aggregator in the Custom Element so that the events are only valid within each separator element).

I found this other discourse post

Something like
ea.publish(SessionCreatedEvent.Channel, new SessionCreatedEvent(params))
…
You don’t need the enum. If you pass an object, the constructor will be used as the key. Same result.

is that what I’m suppose to do to get a transient EA in each component? The doc isn’t clear about such use case.

1 Like

Why not just give each grid a uid when created and include that in all of your messages?

1 Like

I was thinking about that at some point but the problem is that if I create 3 grids, even if I have only 1 event aggregator sent (say “filter-cleared” message), it actually sends 3 messages even if I have a uid (which I tried and it sends 3 messages with same uid which is weird). That is why I’m trying to the Event Aggregator to be somehow transient (where it lives in it’s own component)

1 Like

That doesn’t seem right… If you had it working as I’d expect I see no issue with each listener getting each others messages., but they should only be sent once

Why not just do some custom events then that you stop propogation on.

1 Like

I know that doesn’t seem right but I spent more than 2 days on this and I don’t wish to spend another 2 days on troubleshooting this further. Worst case, I’ll just make my own pub/sub with simple callback methods but I was hoping there was a better way of having independent Event Aggregator… however that would mean rewriting a bunch of code, which I would try to avoid

2 Likes

are you publishing by string or function?

I have have a plugin where I publish using a unique string for the instance which includes the uid and the subscribers also listen for that.

1 Like

right now I have some EA that tells when filters are cleared (passes a boolean), and other events like filter changed (which in that case is the filter object). Was that your question?

But again if I read the doc quote again

If you include the aurelia-event-aggregator plugin using “basicConfiguration” or “standardConfiguration” then the singleton EventAggregator’s API will be also present on the Aurelia object. You can also create additional instances of the EventAggregator, if needed

It does seem to say that there is a way to handle the EA in a transient fashion and be independent in each components, so what is the config to do so? I’m almost sure that I’m just missing a config of the EA somehow.

1 Like

I’ve had zero luck with transient in the past so i gave up

1 Like

Are the requirements:

  • Each grid has a separate EventAggregator.
  • The EventAggregator of each grid needs to be retrievable in any sub resources (elements/attributes/value converter) inside the grid template?

For example, for the template at https://github.com/ghiscoding/aurelia-slickgrid/blob/master/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html, it has a pagination, the pagination custom element will get the event aggregator that is scoped to the slick grid parent, not the global event aggregator?

If so, NewInstance is the resolver that you want. Example here

In that example, I also added an IdentityProvider to demonstrate how it could help separate the identities of Child instances inside a Parent instance

Note: it’s about the scope of the container, transient doesn’t affect that

3 Likes

Wow @bigopon how do you come with these answers so quickly? You’ve even created a sandbox, you’re amazingly fast, I’m just speechless :open_mouth:

Indeed the NewInstance looks like is what I need and to answer your questions, it would be Yes to everything you said… and funny enough the problem I have identified and causing me all these issues is the Pagination (because it’s a custom element (pager) into a custom element (grid)). I won’t use any global events (I might have some now, but I plan to remove them all and just use dispatchEventon the custom element and use the EA only internally).

Thanks a lot @bigopon, your help is very valuable

1 Like

Glad you got it solved.

The doc and the wiki of Aurelia slick grid look amazing, would be nice if you could help with some Aurelia doc later :smiley:

Thanks, I did pass a lot of time on these Wikis, they exist to help me remember how to do certain things and also to avoid having too many questions on GitHub hehe

BTW, I haven’t tried the NewInstance yet, I’m still working on other features but will try in coming days, but I’m quite certain it will work as you explained it well :slight_smile:

1 Like

@elitemike just to point out that Transient is super useful and required in my case for all the Services that I use in Aurelia-Slickgrid and I have like 10 of them (it’s a big plugin) and without Transient in the picture, I wouldn’t be able to create multiple grids in the same page. The problem that I have today is because of a custom element into a custom element, the Pagination that is, (which is different than a service) and @bigopon provided a really great answer… So Transient still has it’s place in the family picture, mainly Services :wink:

2 Likes

@bigopon I can confirm that totally fixes my problem, I only added the NewInstance.of(EventAggregator) in the Parent (the grid custom element) and now the Pagination (child) is working as expected without affecting any other grid… sweet!!!

However, I just have a small issue with my Jest unit tests, they are failing with this message
(note: this unit test can be seen here)

Inner Error:
    Message: #<Object> is not a constructor
    Inner Error Stack:
    TypeError: #<Object> is not a constructor
        at Object.invoke (C:\\OwnCloudDev10\\GitHub\\aurelia-slickgrid\\node_modules\\aurelia-dependency-injection\\dist\\commonjs\\aurelia-dependency-injection.js:387:24)
        at InvocationHandler.Object.<anonymous>.InvocationHandler.invoke (C:\\OwnCloudDev10\\GitHub\\aurelia-slickgrid\\node_modules\\aurelia-dependency-injection\\dist\\commonjs\\aurelia-dependency-injection.js:360:28)
        at Container.Object.<anonymous>.Container.invoke (C:\\OwnCloudDev10\\GitHub\\aurelia-slickgrid\\node_modules\\aurelia-dependency-injection\\dist\\commonjs\\aurelia-dependency-injection.js:546:28)
        at NewInstance.Object.<anonymous>.NewInstance.get (C:\\OwnCloudDev10\\GitHub\\aurelia-slickgrid\\node_modules\\aurelia-dependency-injection\\dist\\commonjs\\aurelia-dependency-injection.js:261:34)
        at Container.Object.<anonymous>.Container.get (C:\\OwnCloudDev10\\GitHub\\aurelia-slickgrid\\node_modules\\aurelia-dependency-injection\\dist\\commonjs\\aurelia-dependency-injection.js:488:24)

I mock the Event Aggregator with the following

const eventAggregator = {
  publish: jest.fn(),
  subscribe: jest.fn()
};
jest.mock('aurelia-event-aggregator', () => ({
  EventAggregator: eventAggregator
}));

and then check that the EA got called with the following test and that throws the console error

  it('should create a grid and expect multiple Event Aggregator being called', async () => {
    await customElement.create(bootstrap);
    expect(eventAggregator.publish).toHaveBeenCalled();
    expect(eventAggregator.publish).toHaveBeenNthCalledWith(1, 'onBeforeGridCreate', true);
    expect(eventAggregator.publish).toHaveBeenNthCalledWith(2, 'onDataviewCreated', expect.any(Object));
    expect(eventAggregator.publish).toHaveBeenNthCalledWith(3, 'onGridCreated', expect.any(Object));

    customElement.dispose();
    expect(eventAggregator.publish).toHaveBeenNthCalledWith(4, 'onBeforeGridDestroy', expect.any(Object));
    expect(eventAggregator.publish).toHaveBeenNthCalledWith(5, 'onAfterGridDestroyed', true);
  });
2 Likes

well the error suggests there is no constructor and looking here at your mock it indeed isn’t new-able. Perhaps that could be the reason

1 Like

Here’s the translation for Vildan (zewa666) post, based on your mock:

const eventAggregator = {
  publish: jest.fn(),
  subscribe: jest.fn()
};
jest.mock('aurelia-event-aggregator', () => ({
  EventAggregator: function() {
    return eventAggregator;
  }
}));
1 Like

alternatively this

const eventAggregator = function() {
  this.publish = jest.fn();
  this.subscribe = jest.fn();
};

should work as well I guess

EDIT:
As @bigopon pointed out correctly my approach would create a new instance every time whereas his re-uses the same object which typically would be favourable.

2 Likes

Awesome thanks to both, it now works and yes I needed the translation, thanks @bigopon and @zewa666

I went for the suggestion and then rewrote it for the shorter ES6 version

jest.mock('aurelia-event-aggregator', () => ({
  EventAggregator: () => eventAggregator
}));

All my tests are all passing again and I can keep my 100% coverage :smiley:
Another version of Aurelia-Slickgrid is on the horizon…

Thanks a bunch!!!

1 Like

I ended up using a class factory instead of injection for my needs. I may revisit it, but I need to do some extra logic in the factory to set some properties anyways that can’t be completely set during injection. Can I make injection work, maybe. Is it worth my time to redo something that works… Maybe when I’m bored.

Glad you got everything working

When you do transient. Are you just doing @transient decorator?

2 Likes

I had another user helping me for that part, what he found to work best is to add on top of each Service @singleton(true). The documentation says the following:

singleton(overrideChild?:boolean) - Normally, types are auto-registered as singletons in the root container. So, why do we provide this decorator? This decorator allows you to specify true as an argument to indicate that the singleton should be registered not in the root container, but in the immediate container to which the initial request was issued.

2 Likes