Access isExplicitNavigationBack from router:navigation:complete event - aurelia

Aurelia doesn't automatically scroll the browser to the top of the page after navigating to a new route, so I've easily solved this using the EventAggregator, listening to the router:navigation:complete event inside my main App class (app.js):
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
#inject(EventAggregator)
export class App {
constructor (ea) {
this.ea = ea;
this.ea.subscribe('router:navigation:complete', e => {
document.body.scrollTop = document.documentElement.scrollTop = 0;
});
}
}
I recently noticed the isExplicitNavigationBack property on the router which would be extremely useful to prevent top scrolling when the user navigates backwards, however, the property is always false. I've tried with back button as well as Router.navigateBack().
My hope was to simply change my subscription callback to this:
if (!e.instruction.router.isExplicitNavigationBack) {
document.body.scrollTop = document.documentElement.scrollTop = 0;
}
But unfortunately isExplicitNavigationBack is always false - why?

In the commit adding the isExplicitNavigationBack property you can see that the value is set to true when calling router.navigateBack() but it's set back to false on resolveInstruction.
Since you subscribed to the router:navigation:complete event, the routing is already complete and therefore the value is set back to false. If you subscribe to the router:navigation:processing event, it should be true if you call router.navigateBack().
this.ea.subscribe('router:navigation:processing', e => {
if (!e.instruction.router.isExplicitNavigationBack) {
document.body.scrollTop = document.documentElement.scrollTop = 0;
}
});

Related

Vuejs v-for so laggy when infinite scrolling

I have this weird vuejs effect where when I am adding a new object data, the v-for re-renders all the object even if its already rendered.
I am implementing an infinite scroll like facebook.
The Code
To explain this code, I am fetching a new data from firebase and then push the data into the data object when it reaches the bottom of the screen
var vueApp = new Vue({
el: '#root',
data: {
postList: [],
isOkaytoLoad: false,
isRoomPostEmpty: false,
},
mounted: function() {
// Everytime user scroll, call handleScroll() method
window.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll: function()
{
var d = document.documentElement;
var offset = d.scrollTop + window.innerHeight;
var height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height)
{
this.isOkaytoLoad = false;
(async()=>{
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
roomSnapshot.forEach((doc) => {
const postID = doc.id;
(async()=>{
// Put the new data at the postList object
this.postList = [...this.postList, doc];
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc['commentCount'] = commentSnapshot.size;
//this.postList.push(doc);
console.log(this.postList);
setTimeout(()=>{ this.isOkaytoLoad = true }, 1000);
})();
});
})();
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div v-if="postList.length > 0" class="card-containers">
<!-- I have a component `Postcard` in my js file and it works well -->
<Postcard
v-for="post in postList"
:key="post.id"
:post-id="post.id"
:owner-name="post.data().owner_displayName"
:owner-uid="post.data().owner_id"
:post-type="post.data().post_type"
:image-url="post.data().image_url"
:post-content="truncateString(linkify(post.data().post_content))"
:room="post.data().room"
:time="post.data().time.toDate()"
:likers="post.data().likers"
:comment-count="post.commentCount"
:file-url="post.data().file_url"
:file-name="post.data().file_name"
:downloads="post.data().downloads">
</Postcard>
</div>
Now, the problem is here ...
Look at this screen record, FOCUS AT THE MOUSE, it's lagging and I can't even click on those buttons when vuejs is adding and loading the new data
Here is the code That I used
What I suspect
I am suspecting that everytime I add a new data, the VueJS re-renders it all, which does that effect. How can I force vueJS to not re-render those data that is already rendered in the screen?
You've got two unnecessary async IIFE; the second one inside the forEach is particularly problematic because the async code inside it will be executed concurrently for each loop iteration, which has implications:
getDocs() will be fired all at once for each loop ieration, potentially spamming the server (assuming this is performing a network request). Was this your intention? It looks like you're only fetching at most 5 new posts, so this is probably OK.
The async function updates some state which will trigger Vue to re-render for each doc. This should be batched together at the end so Vue does as minimal updates as possible.
Also don't use var; use const or let instead. There's almost no good reason to use var anymore, let it die.
I can't say this will improve your performance substantially, but I recommend making the following changes (untested):
async handleScroll() {
const d = document.documentElement;
const offset = d.scrollTop + window.innerHeight;
const height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height) {
// Prevent loading while we load more posts
this.isOkaytoLoad = false;
try {
// Get new posts
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
// Fetch comments of each post. Do this all at once for each post.
// TODO: This can probably be optimized into a single query
// for all the posts, instead of one query per post.
await Promise.all(roomSnapshot.docs.map(async doc => {
const postID = doc.id;
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc.commentCount = commentSnapshot.size;
}));
// Append the new posts to the list
this.postList.push(...roomSnapshot.docs);
} catch (ex) {
// TODO: Handle error
} finally {
// Wait a bit to re-enable loading
setTimeout(() => { this.isOkaytoLoad = true }, 1000);
}
}
}
Doing :post-content="truncateString(linkify(post.data().post_content))" in the template means linkify will be executed during each re-render. I suspect linkify may be potentially slow for long lists? Can this be pre-calculated for each post ahead of time?
You're registering a window scroll event listener when the component is mounted. If the component is ever destroyed, you need to unregister the event listener otherwise it'll still fire whenever the window is scrolled. This may not be an issue in your case, but for reusable components it must be done.

vee validate 3.x setting all fields to touched on submit

How can I set the fields in my form that are wrapped in ValidationProvider to touched when submitting a form. My current code is as follows using the ValidationObserver with a ref of "observer" to wrap the form. This changes the fields object in the observer but does not reflect in my ValidationProvider as being touched as I only show error when touched and error exists and the touched part is not being triggered.
onSubmit () {
const observer = this.$refs.observer as any
return observer.validate().then((valid: boolean) => {
if (!valid) {
for (const field in observer.fields) {
observer.fields[field].touched = true
observer.fields[field].untouched = false
}
}
return valid
})
}

React-Navigation and creation/destroy os screens/components

I have my app with some screens. One of these screens is called "Race". This race produce a ListView with current race ranking (came from Redux state). This same screen has a TCP connection component which connect to my sensor and get data (async).
When user click on rank item, a new screen is opened with current lap's from clicked racer (item on ListView). At this point, previous screen still working (which is fine), because I can see in my log when data is received by TCP component.
The problem is when user hit back buttom (or navigate to "Race" using side menu), the screen itself is re-created and my TCP component is re-created, which I don't want. So my question is: how can I prevent this screen to be re-constructed OR make this TCP component works like a singleton globally? I'm not sure if is possible or how to make it work with Redux.
Update 1: This is part of code. In this Race screen, I have this internal function that connect my TCP socket.
_connectSensor() {
console.log("Running connectSensor function...");
const { lap_info, dispatchAddLap, dispatchSetSensorStatus } = this.props;
const { race, sensor } = this.props;
if (sensor.sensor_status == SENSOR_ONLINE) {
console.log("Already connected!");
return;
}
dispatchSetSensorStatus(SENSOR_CONNECTING);
//var serverHost = sensor.host;
//var serverPort = sensor.port;
var serverHost = "mydomain.com";
var serverPort = 1212;
console.log("Sensor host: ",serverHost);
console.log("Sensor port: ",serverPort);
this.client = net.createConnection(serverPort, serverHost, () => {
// client.write('Hello, server! Love, Client.');
dispatchSetSensorStatus(SENSOR_ONLINE);
});
this.client.on('data', (data) => {
var obj = JSON.parse(data);
dispatchAddLap(obj);
});
This function is called by a button and it's working fine. When I go to another screen, this TCP socket still running and feeding data to my Redux, which is the desirable state. But when I got back to this screen (from another one), I can't access this socket anymore.....they still running and I can't stop. When I click "Start", "this.client" is a new object because react-navigation re-create my entire screen (my guess....).
I could put some code to force disconnect when this screen is ummounted....but is not what I need. I need to keep socket receiveing data (if user doesn't stop), even if this screen is not active.
Update 2: I have tried to add static client = null; to my class object, but doesn't work.
In my "Stop" button, I've added to debug console.log("Client object:",this.client); and this is the result:
1) When I open race screen and click 'start', then 'stop', object is returned for 'client'.
2) When I open race screen, click start, goe to another screen, go back to race screen and click "Stop": undefined is returned for 'client' object.
Solved. The 'key' for this problem is to export my class as new object, and not just class.
But I had another problem: if I need to use 'connect' from 'react-redux', this doesn't work....simple because 'connect' always return a new object, not the same.
So I made my own ocmponent and create functions to assign dispatchFunctions coming from parent object. This is my final TCP componente:
import React, { Component } from "react";
import { SENSOR_ONLINE, SENSOR_OFFLINE, SENSOR_CONNECTING } from "../constants";
var net = require("react-native-tcp");
class tcpCon extends Component {
static myId;
static dispatchAddLap;
static dispatchSetSensorStatus;
static client;
static sensor_status = SENSOR_OFFLINE;
constructor(props) {
super(props);
console.log("Componente TCP criado! Dados passados: ", props);
var RandomNumber = Math.floor(Math.random() * 10000 + 1);
this.myId = RandomNumber;
}
setDispatchAddLapFunc(func) {
this.dispatchAddLap = func;
}
setDispatchSetSensorStatusFunc(func) {
this.dispatchSetSensorStatus = func;
}
disconnectSensor() {
console.log("Disconnecting....Status no componente TCP: ", this.sensor_status);
if (this.sensor_status == SENSOR_ONLINE) {
this.sensor_status = SENSOR_OFFLINE;
// Dispatch sensor offline
if (this.client) {
console.log("Client object exists. Need to destroy it....");
this.client.destroy();
this.client = null;
} else {
console.log("Client doesn't exists");
}
}
}
displayObj() {
console.log("Client nesta conexao:",this.client);
}
connectSensor(hostname, port) {
console.log("Running connectSensor function...myId: ", this.myId);
console.log("Meu status local:", this.sensor_status);
if (this.sensor_status == SENSOR_ONLINE || this.client) {
console.log("Connection already exists! Returning....");
return;
}
//var con = net.createConnection(port, hostname, () => {
this.client = net.createConnection(port, hostname, () => {
// client.write('Hello, server! Love, Client.');
// dispatchSetSensorStatus(SENSOR_ONLINE);
//this.client = con;
this.sensor_status = SENSOR_ONLINE;
this.dispatchSetSensorStatus(SENSOR_ONLINE);
});
this.client.on("data", data => {
var obj = JSON.parse(data);
//console.log("Data arrived: ",obj);
this.dispatchAddLap(obj);
});
this.client.on("error", error => {
console.log("Erro conectando ao sensor: " + error);
// dispatchSetSensorStatus(SENSOR_OFFLINE);
this.sensor_status = SENSOR_OFFLINE;
this.dispatchSetSensorStatus(SENSOR_OFFLINE);
});
this.client.on("close", () => {
console.log("Conexão fechada no componente TCP.");
// dispatchSetSensorStatus(SENSOR_OFFLINE);
this.sensor_status = SENSOR_OFFLINE;
this.dispatchSetSensorStatus(SENSOR_OFFLINE);
});
//console.log("Client object na connection:", this.client);
//this.dispatchAddLap('Hello world!ID '+this.myId.toString());
}
}
export default new tcpCon();
And this is how I call to connect:
Con.setDispatchAddLapFunc(dispatchAddLap);
Con.setDispatchSetSensorStatusFunc(dispatchSetSensorStatus)
Con.connectSensor("address",1212);
To disconnect, just use this:
Con.setDispatchAddLapFunc(dispatchAddLap); // Just to be sure
Con.setDispatchSetSensorStatusFunc(dispatchSetSensorStatus); // Just to be sure
Con.disconnectSensor();
With this, anytime that I call this component, the same object is returned.

Aurelia, check when DOM is compiled?

How to check when DOM is compiled and inserted from Aurelia repeat cycle when the model is updated?
I have the following html:
<div clas="parent">
<div class="list-group">
<a repeat.for="$item of treeData">${$item.label}</a>
</div>
</div>
Here I need to know when all <a> tags are listed in the DOM, in order to run jquery scroll plugin on the parent <div> container.
At first load, I do that from the attached() method and all is fine.
When I update the treeData model from a listener, and try to update the jquery scroll plugin, it looks that the DOM is not compiled, so my scroll plugin can not update properly.
If I put timeout with some minimum value like 200ms it works, but I don't think it is a reliable workaround.
So is there a way to solve that?
Thanks!
My View Model:
#customElement('tree-view')
#inject(Element, ViewResources, BindingEngine)
export class TreeView {
#bindable data = [];
#bindable filterFunc = null;
#bindable filter = false;
#bindable selectedItem;
constructor(element, viewResources, bindingEngine) {
this.element = element;
this.viewResources = viewResources;
this.bindingEngine = bindingEngine;
}
bind(bindingContext, overrideContext) {
this.dataPropertySubscription = this.bindingEngine
.propertyObserver(this, 'data')
.subscribe((newItems, oldItems) => {
this.dataCollectionSubscription.dispose();
this._subscribeToDataCollectionChanges();
this.refresh();
});
this.refresh();
if (this.filter === true) {
this.filterChanged(this.filter);
}
if (this.selectedItem) {
this.selectedItemChanged(this.selectedItem);
}
}
attached() {
$(this.element).perfectScrollbar();
}
refresh() {
this.treeData = processData(this.data, this.filterFunc);
this.listItemMap = new WeakMap();
this.treeData.forEach(li => this.listItemMap.set(li.item, li));
this.filterChanged(this.filter);
$(this.element).perfectScrollbar('update');
}
This is only part of the code, but most valuable I think.
I attach the jq plugin in attached function and try to update it in refresh function. In general I have listener that track model in other view, which then update that one without triggering bind method.
An approach would be to use something called window.requestAnimationFrame (https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
In your view-model, when you modify your treeData array, try calling
window.requestAnimationFrame(()=>{
$.fn.somePlugin();
});
Haven't tested this out, but based off what you're telling me, this might do what you need.
You could push your code onto the microTaskQueue, which will schedule your function to be executed on the next event loop. For instance:
import { TaskQueue } from 'aurelia-task-queue';
//...
#inject(Element, ViewResources, BindingEngine, TaskQueue)
export class TreeView {
constructor(element, viewResources, bindingEngine, taskQueue) {
this.element = element;
this.viewResources = viewResources;
this.bindingEngine = bindingEngine;
this.taskQueue = taskQueue;
}
refresh() {
this.treeData = processData(this.data, this.filterFunc);
this.listItemMap = new WeakMap();
this.treeData.forEach(li => this.listItemMap.set(li.item, li));
this.filterChanged(this.filter);
// queue another task, which will execute after the tasks queued above ^^^
this.taskQueue.queueMicroTask(() => {
$(this.element).perfectScrollbar('update');
});
}
}

durandaljs child router deactivate event

How to fire deactivate event when parent route changing.
For instance in 'HTML Samples', when Master-Detail page is active, change view to another one.
How to force dialog "Do you want to leave...." here?
Thanks
Vladimir.
UPD:
Code from HTML Samples with replaced dialogs
project.js
define(['durandal/system', 'durandal/app'], function (system, app) {
var ctor = function (name, description) {
this.name = name;
this.description = description;
};
ctor.prototype.canActivate = function () {
return true; //!!! CHANGED!!!
};
ctor.prototype.activate = function () {
system.log('Model Activating', this);
};
ctor.prototype.canDeactivate = function () {
return false; //!!! CHANGED!!!
};
ctor.prototype.deactivate = function () {
system.log('Model Deactivating', this);
};
return ctor;
});
Now you can not change detail view using select control.
But easily can change whole module via navigation panel.
What you're looking for is the canDeactivate attribute. You can use the dialog plugin to display a dialog. If your canDeactivate function returns true, the view will deactivate. If it returns false, the view will remain.
Update
There is a bug in the router, that is a wontfix, where a child viewModel is not properly deactivated when the parent viewModel is deactivated / navigated away from. See here: https://github.com/BlueSpire/Durandal/issues/570
See
http://durandaljs.com/documentation/Using-Activators/
http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks/
This is a super late answering. I am maintaining an existing durandal app. The quick fix of mine is to manually call deactivate of child viewmodel in the deactivate of the parent. However, the child deactivate may be called multiple times.
Shell.prototype.deactivate = function(){
this.router.activeItem().deactivate();
};