Aurelia EventAggregator not subscribing - aurelia

I have a parent-child component architecture in my Aurelia app. Panel is a Parent component View-model, inside which there is a Tool component.
I have a pagination UI on Panel, clicking on which the Tool should update. The problem is, the variable which keep track of which page number was clicked, pageNumber is only available in panel.ts and not available in tool.ts. So basically, this is an issue of communicating between two ViewModels.
To solve this issue, I am using Aurelia's EventAggregator by following this excellent tutorial. Here is what I have written till now:
panel.html
<a class="page-link" click.delegate="pageClick(1)"> 1 </a>
panel.ts
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
#inject(EventAggregator)
export class Panel {
eventAggregator: EventAggregator;
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
}
pageClick(pageNumber) {
var pageInfo = {
pageNumber: pageNumber
}
this.eventAggregator.publish("pageClicked", pageInfo);
}
tool.ts
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
#inject(EventAggregator)
export class Tool {
eventAggregator: EventAggregator;
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
}
pageClicked() {
this.eventAggregator.subscribe("pageClicked",
pageInfo => {
console.log(`${pageInfo.pageNumber} was clicked`);
});
}
This works fine until the event is fired. I tried debugging and saw that eventAggregator fired the pageClicked event. But the breakpoint on subscribe was never hit. Somehow the subscribe method is not triggered. What am I missing here?
My initial thought is that EventAggregator instance is different, but I am not sure if it needs to be same. Any help is appreciated. Also, if you know some other better way to achieve intercomponent communication please let me know how. Thanks.

You need to set up the subscription in a function that will be called. Maybe add an attached callback and set up the subscription there. Make sure to dispose the subscription, probably in a detached callback. I'm on mobile right now, but if you need a code example, let me know, and I'll add one when I get home.

Related

Vue.js Create a helper class to call your methods globally

I have just started my first project with Vue.js, I have managed to do a lot of basic things and now I am trying to structure the project. I want to achieve the highest possible code reuse. One of the most frequent cases of my application is going to be showing messages of different types, confirmation, information, etc. For this reason, I want to create a mechanism that allows me to launch these messages globally, regardless of where I call them.
As far as I have been able to advance, I have opted for the following variant:
1- I have created a directory called classes in my src directory.
2- I have created a file called MessageBox.js inside classes directory with the following content:
import Vue from 'vue';
export default class MessageBox extends Vue {
confirm() {
return alert('Confirm');
}
information() {
return alert('Information');
}
}
I define it like this because I want to call these methods globally as follows:
MessageBox.confirm();
I am really new to Vue.js and I was wondering if there is any other way to achieve the results I am looking for in a more efficient way .... or .. maybe more elegant?
Thank you very much in advance..
There are at least 2 ways of going about this:
Event bus
Rely on Vue.js internals to create a simple EventBus. This is a design pattern used in Vue.js.
Create a file and add the following lines to it
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
Create your component that takes care of displaying global dialogs. This is usually registered at the top of the tree, so it can cover the entire real estate.
Import the event bus import EventBus from 'event_bus' and then register for the new events
EventBus.$on('SHOW_CONFIRM', (data) => {
// business logic regarding confirm dialog
})
Now you can import it in any component that wants to fire an event like so
EventBus.$emit('SHOW_CONFIRM', confirmData);
Vuex
You can also use vuex to store global data regarding dialogs and add mutations to trigger the display of the dialogs.
Again, you should define a component that takes care of displaying and push it towards the top of the visual tree.
Note: in both cases you should handle cases in which multiple dialog need to be shown at the same time. Usually using a queue inside the displaying component works.
It's an antipattern in modern JavaScript to merge helper functions that don't rely on class instance into a class. Modules play the role of namespaces.
Helper functions can be defined as is:
messageBox.js
export function confirm() {
return alert('Confirm');
}
They can be imported and used in component methods. In case they need to be used in templates, they can be assigned to methods where needed one by one:
Some.vue
import { confirm } from './util/messageBox';
export default {
methods: { confirm }
}
Or all at once:
import * as messageBox from './util/messageBox';
export default {
methods: { ...messageBox }
}
Helpers can be also be made reusable as Vue mixins:
messageBox.js
...
export const confirmMixin = {
methods: { confirm };
}
export default {
methods: { confirm, information };
}
And used either per component:
Some.vue
import { confirmMixin } from './util/messageBox';
export default {
mixins: [confirmMixin]
}
Or globally (isn't recommended because this introduces same maintenance problems as the use of global variables):
import messageBoxMixin from './util/messageBox';
Vue.mixin(messageBoxMixin);

How to add Mobx observer to LitElement

I have the following component, my component correctly displays the message from appState but when I change the value of appState the component isn't updated. I know I need to add an #observer, but how do you add it to a LitElement?
import { LitElement, html } from 'lit-element';
import { observable } from "mobx";
var appState = observable({
message: 'World'
});
class MyElement extends LitElement {
handleClick() {
appState.message = 'All';
}
render(){
return html`
<p>Hello, ${appState.message}</p>
<button #click=${this.handleClick}>Click me</button>
`;
}
}
customElements.define('my-element', MyElement);
LitElement itself is not such a good fit for mobx as changes which trigger a render need to be "full changes". Changing a property of an object is still the same object instance e.g. it will not trigger a render.
You can read the full story at https://open-wc.org/faq/rerender.html
You probably could use mobx autorun to trigger this.updateComplete() to force rerender but in that case, it's probably better to use a specialised lit-element version like https://github.com/adobe/lit-mobx.
Alternatively, a state machine could be a good fit in many cases as well. Take a look at https://www.npmjs.com/package/lit-robot.

Custom element based on bootstrap-toggle not updating attribute binding

I have created a custom element based on bootstrap-toggle that looks as follows:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle();
}
}
toggle.html:
<template>
<require from="bootstrap-toggle/css/bootstrap2-toggle.min.css"></require>
<require from="bootstrap-toggle"></require>
<input ref="input" data-toggle="toggle" type="checkbox" checked.bind="checked">
</template>
The problem ist that the binding for the checked attribute is never updated when the switch is toggled via the UI. I am aware of the common pitfalls when using Aurelia with jQuery based components as described here. However, in my understanding this should not apply to bootstrap-toggle, as this component triggers a change event on the input element on toggle. I have verified that this change event bubbles up to my custom component.
The workaround I currently use is this:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle().on('change', (event) => this.checked = event.target.checked);
}
}
However, I do not understand why this should be necessary.
I have created a test project based on Aurelia's navigation-skeleton that can be downloaded here.
I would appreciate some help in understanding this!
The event listener used by jquery uses the capture phase and presumably does preventDefault or something among those lines that prevents the event from bubbling back up.
The change event dispatched by this line: if (!silent) this.$element.change() does not actually propagate to the bubble phase. Aurelia uses the bubble phase and thus never receives the event.
You can see the difference if you dispatch one manually in your browser console like so:
document.querySelector("[data-toggle]").dispatchEvent(new CustomEvent("change"));
This will result in the appropriate listeners being invoked and updates the bound property.
What this line does: .on('change', (event) => this.checked = event.target.checked); is also add a listener on the capture phase which then of course works. But then you might as well remove the checked.bind='checked' because that effectively does nothing.
There isn't necessarily any straight-forward fix for this, these CSS frameworks tend to just have very intrusive javascript. I would generally recommend against including their scripts and look for (or implement) them natively in Aurelia. It's very easy to do so and just works a lot better.

Aurelia: How to track if #children elements are loaded?

I have custom Tabs functionality, but after the update of Aurelia to rc-1.0.x, there is issue with listing data from #children decorator.
My code look something like:
import {inject, customElement, children} from 'aurelia-framework';
#customElement('tabs')
#inject(Element)
#children({name:'tabs', selector: "tab"})
export class Tabs {
activeTab = undefined;
constructor(element) {
this.element = element;
}
attached() {
console.log(this.tabs); // Return undefined on promise resolve!!!
this.tabs.forEach(tab => {
if (tab.active) {
this.activeTab = tab;
}
tab.hide();
});
this.activeTab.show();
}
On first load everything is working just fine, and this.tabs is an array of items, as expected.
Next if I do a server request, when promise is resolved this.tabs console logs undefined.
If I set timeout it fix the issue, but is that the correct way?
Also I noticed in the html, that the repeat.for statement is executed, which give me a clue that this.tabs is received with some delay, after the attached function is handled.
The html:
<template>
<ul class="nav nav-tabs m-b-1">
<li repeat.for="tab of tabs">
<a href="#" click.trigger="$parent.onTabClick(tab)">
${tab.name & t}
</a>
</li>
</ul>
<slot></slot>
</template>
So is there a way to make that work with the Aurelia bind or attached methods or some more elegant way, instead to check the value of this.tabs with a timeout function?
I think this might be an adequate use-case for the Task Queue (but if someone more knowledgeable thinks otherwise, let me know). I have had to use the Task Queue before when accessing bindable values from inside of attached and your instance looks basically the same (except you're accessing them using #children).
The TaskQueue will push your logic to the end of the processing stack. So theoretically this means it will wait for Aurelia to finish running its internal logic for bindings and possibly children decorator resolution and then run your code.
There might be a better solution, but I have used this before to get out of a similar situation as I mentioned earlier and have yet to find a better solution that fixes the problem, specifically in regards to accessing dynamic values inside of attached.
import {TaskQueue} from 'aurelia-framework';
#customElement('tabs')
#inject(Element, TaskQueue)
#children({name:'tabs', selector: "tab"})
export class Tabs {
activeTab = undefined;
constructor(element, taskQueue) {
this.element = element;
this.taskQueue = taskQueue;
}
attached() {
this.taskQueue.queueMicroTask(() =>{
this.tabs.forEach(tab => {
if (tab.active) {
this.activeTab = tab;
}
tab.hide();
});
this.activeTab.show();
});
}

Using humane.js with aurelia

I'm trying to use humane.js with aurelia however I'm running in a problem.
It appears humane.js adds an element to the DOM when it's created and so far the only way I've found to do it is to force it like this....
showMessage(message) {
this.notify = humane.create();
this.notify.log(message);
}
However this creates a new instance of humane every time showMessage() is called. This breaks the queue as each one is rendered separately.
I've tried putting the create() in the activate() method of the view model but that doesn't seem to work either.
Any ideas?
This solved the problem, I've created a custom element for humane that is then included in app.html in the same way loading-indicator is in the skeleton app.
import humane from 'humane-js';
import 'humane-js/themes/original.css!';
import {inject, noView} from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';
import { ApiStatus } from 'resources/messages';
#noView
#inject(EventAggregator)
export class StatusIndicator {
constructor(ea) {
this.ea = ea;
ea.subscribe(ApiStatus, msg => this.showMessage(msg.apistatus));
}
attached() {
this.humane = humane.create();
}
showMessage(message) {
this.humane.log(message);
}
}
The important part was the attached() this allows the setup of humane to work correctly.
Unfortunately for Aurelia, Humane will attach itself to the DOM automatically as a child of body, which Aurelia then replaces.
There is a really, really, simple fix for this:
Change your:
<body aurelia-app="main">
To this:
<body><div aurelia-app="main">
This way, Aurelia doesn't replace the div which is in body, you don't need to worry about attached() or where the import appears in your code, and humane works perfectly.
I have raised a humane github issue for this. https://github.com/wavded/humane-js/issues/69
Here is how I am using humane.js with Aurelia:
1) I load the CSS in the app index.html.
2) In each view model that requires humane, I import humane
import humane from 'humane-js/humane';
I do NOT inject human into the view model.
3) I show notifications like this:
humane.log('Error:, { addnCls: 'humane-libnotify-error' });
I hope this helps you.