Why won't my MithrilJS UI render unless I call redraw after a programmatic redirect? - mithril.js

Here is a snippet of view code. Why won't it work without the m.redraw()? If I don't call that, the route changes and the login controller loads, but nothing is rendered into the DOM.
home.view = function(ctrl) {
console.log('in home view');
if (!mo_portal.logged_in) {
console.log('redirecting to login');
m.route("/login");
m.redraw();
return;
}
return m("div","HOME");
}

Changing route will always trigger a redraw. If you're not seeing the login page view without manually calling m.redraw it's probably down to bugs in the login controller or view that happen during the route change redraw - bugs whose failure conditions are reset when you call m.redraw again.
Here's an extension of your code with a login view and controller. mo_portal.logged_in is set to true or false depending on whether the user is one in the usersList or not, so we can test success and failure.
I took out the m.redraw (I also put the redirect logic in the home controller) and everything works fine.
var usersList = [
'john',
'alexia'
];
var mo_portal = {
username : '',
logged_in: false
};
var login = {};
login.controller = function(){
this.username = function( input ){
if( arguments.length ){
mo_portal.username = input;
mo_portal.logged_in = !!~usersList.indexOf( input );
}
return mo_portal.username;
};
};
login.view = function(ctrl){
console.log('in login view');
return [
m( 'input', { oninput : m.withAttr( 'value', ctrl.username ), value : ctrl.username() } ),
m( 'a[href=/home]', { config : m.route }, 'Login' )
];
};
var home = {};
home.controller = function(){
if (!mo_portal.logged_in) {
console.log('redirecting to login');
m.route("/login");
}
};
home.view = function(ctrl) {
console.log('in home view');
return m("div","HOME");
};
m.route( document.body, '/login', {
'/login' : login,
'/home' : home
} );
<script src="https://rawgit.com/lhorie/mithril.js/next/mithril.js"></script>

I think that's not how mithril is going to be used. Mithril does not expect a view update (aka redraw) during ongoing view construction.
I suppose you should do the route-change in the related controller.
Keep in mind, that a view is rendered evertime the page changes somehow. You probably don't want to check the login state evert time.

Related

I have event duplication after action was moved in store object

In my laravel 5.8 / vue 2.5.17 / vuex^3.1.0 I have a problem that with dialog opened I have event duplication.
I have an event for item deletion :
In my vue file:
...
mounted() {
bus.$on('dialog_confirmed', (paramsArray) => {
if (paramsArray.key == this.deleteFromUserListsKey(paramsArray.user_list_id)) {
this.runDeleteFromUserLists(paramsArray.user_list_id, paramsArray.index);
}
})
bus.$on('onUserListDeleteSuccess', (response) => {
this.is_page_updating = false
this.showPopupMessage("User lists", 'User\'s list was successfully deleted!', 'success');
})
bus.$on('onUserListDeleteFailure', (error) => {
this.$setLaravelValidationErrorsFromResponse(error.message);
this.is_page_updating = false
this.showRunTimeError(error, this);
this.showPopupMessage("User lists", 'Error adding user\'s list !', 'error');
})
}, // mounted() {
methods: {
confirmDeleteUserList(user_list_id, user_list_title, index) {
this.confirmMsg("Do you want to exclude '" + user_list_title + "' user list ?", {
key: this.deleteFromUserListsKey(user_list_id), user_list_id: user_list_id, index: index
}, 'Confirm', bus);
}, //confirmDeleteUserList(id, user_list_title, index) {
deleteFromUserListsKey(user_list_id) {
return 'user_list__remove_' + user_list_id;
},
runDeleteFromUserLists(user_list_id, index) {
this.$store.dispatch('userListDelete', { logged_user_id : this.currentLoggedUser.id, user_list_id : user_list_id } );
}, // runDeleteFromUserLists() {
and in resources/js/store.js :
state : {
...
userLists: [],
...
actions : {
userListDelete(context, paramsArray ) {
axios({
method: ( 'delete' ),
url: this.getters.apiUrl + '/personal/user-lists/' + paramsArray.user_list_id,
}).then((response) => {
let L = this.getters.userLists.length
for (var I = 0; I < L; I++) {
if (response.data.id == this.getters.userLists[I].id) {
this.getters.userLists.splice(this.getters.userLists.indexOf(this.getters.userLists[I]), 1)
context.commit('refreshUserLists', this.getters.userLists);
break;
}
}
bus.$emit( 'onUserListDeleteSuccess', response );
}).catch((error) => {
bus.$emit('onUserListDeleteFailure', error);
});
}, // userListDelete(context, paramsArray ) {
confirmMsg (based on https://github.com/euvl/vue-js-modal )is defined in my mixing :
confirmMsg: function (question, paramsArray, title, bus) {
this.$modal.show('dialog', {
title: title,
text: question,
buttons: [
{
title: 'Yes',
default: true, // Will be triggered by default if 'Enter' pressed.
handler: () => {
bus.$emit('dialog_confirmed', paramsArray);
this.$modal.hide('dialog')
}
},
{
title: '', // Button title
handler: () => {
} // Button click handler
},
{
title: 'Cancel'
}
]
})
},
it worked ok, until I moved userListDelete method from my vue file into store.js.
As a result on 1st event item is deleted ok, the the second item raise error that item was not found and I do not know event is doubled...
How to fix it ?
UPDATED BLOCK :
I still search for valid decision :
I uploaded live demo at :
http://178.128.145.48/login
demo#demo.com wdemo
http://178.128.145.48/websites-blogs will be opened.
Please, try to go to “User's lists” by link at top left menu https://prnt.sc/nq4qiy
and back several times. When on “User's lists” page I try to delete 1 user list it is deleted, but I got several messages
and url in “network” section of my browser : https://imgur.com/a/4ubFB0g
Looks like events are duplicated. And looks like that is move between pages number of guplications is raised.
Why and how to fix it ?
I use #click.prevent in triggering the event to show confirm delete message.
There is “ Add Demo Data” to add more demo rows.
Thanks!
Well, it is quite obvious.
Take a closer look at the Vue component lifecycle diagram.
Your component is mounted each time you enter a route.
So, bus.$on inside your mounted block executed each time you enter this route.
I suggest you move bus event handlers to some other location. For example app.js/ App.vue mounted hook or directly into the store. Since all you do inside handler is calling store actions.

Asynchrounous call issue in Angular 5

I am working with Angular5 , I have a big issue (please see the below code)
In simple words it is a asynchronous execution issue, how to reduce this
let confirmData = {
dlgHeader: 'Add',
msgTxt: 'Are you sure to Add Language!!',
eventName: 'locationmanagement_languages_assign'
};
this.confirmService.confirmData = confirmData;
(1) this.confirmationService.setShowConfirm(true);
(2) this.confirmAnchor.createConfirmation(ConfirmationComponent);
this.confirmService.getReturnValue()
.subscribe(
suc => {
if (suc.eventName == 'locationmanagement_languages_assign') {
this.assignLanguage(suc);
}
});
in above (1) line of code is responsible to create confirmation.(i have created custom confirmation component)
inside custom confirmation user can click CONFIRM/CANCEL buttons.
I want to stop (2) line code execution until user click CONFIRM/CANCEL buttons in custom confirmation component.
Now i am using as below in language.component.ts ,, but i am calling the getReturnValue() in ngOnInit().
I dont want to use ngOnInit() to get action from custom confirmation component either it is CONFIRM/CANCEL action
ngOnInit() {
this.getReturnValueEvent();
}
assignLanguageEvent() {
debugger;
this.requestedData = [];
for (let data of this.selectedAssignLanguages) {
this.requestedData.push({
id: data.value.id,
set_value: data.value.setValue
});
}
console.log('Requested Data::', this.requestedData);
let confirmData = {
dlgHeader: 'Add',
msgTxt: 'Are you sure to Add Language!!',
eventName: 'locationmanagement_languages_assign'
};
this.confirmService.confirmData = confirmData;
this.confirmationService.setShowConfirm(true);
this.confirmAnchor.createConfirmation(ConfirmationComponent);
}
getReturnValueEvent() {
this.subscription1 = this.confirmService.getReturnValue()
.subscribe(
suc => {
if (suc.eventName == 'locationmanagement_languages_assign') {
this.assignLanguage(suc);
}
}
);
}

Why are the views in my polymer app not getting loaded?

I am working on creating a Polymer app for a pet project, using the Polymer Starter Kit, and modifying it to add horizontal toolbar, background images, etc. So far, everything has worked fine except the links in the app-toolbar do not update the "view" when I click on them.
All my debugging so far points me in the direction of the "page" property. I believe this is not getting updated or is null, causing the view to default to "about" (which is View-2 as per the starter kit) as specified in the _routePageChanged observer method.
I tried using the debugger on DevTools on Chrome, but being new to this, I'm not very clear if I did it correctly. I just kept going in and out of hundred of function calls.
I am copying relevant parts of the app-shell.
Please help or at least point me in the right direction; I've been trying to fix this since 2 days. Thank you!
<app-location
route="{{route}}">
</app-location>
<app-route
route="{{route}}"
pattern=":view"
data="{{routeData}}"
tail="{{subroute}}">
</app-route>
<!-- Main content -->
<app-header-layout has-scrolling-region>
<app-header slot="header" class="main-header" condenses effects="waterfall">
<app-toolbar class="logo"></app-toolbar>
<app-toolbar class="tabs-bar" hidden$="{{!wideLayout}}">
<paper-tabs selected="[[selected]]" attr-for-selected="name">
<paper-tab>Home</paper-tab>
<paper-tab>About Us</paper-tab>
<paper-tab>Pricing</paper-tab>
</paper-tabs>
</app-toolbar>
</app-header>
<iron-pages
selected="[[page]]"
attr-for-selected="name"
fallback-selection="view404"
role="main">
<my-view1 name="home"></my-view1>
<my-view2 name="about"></my-view2>
<my-view3 name="pricing"></my-view3>
<my-view404 name="view404"></my-view404>
</iron-pages>
</app-header-layout>
</app-drawer-layout>
<script>
class MyApp extends Polymer.Element {
static get is() { return 'my-app'; }
static get properties() {
return {
page: {
type: String,
reflectToAttribute: true,
observer: '_pageChanged'
},
wideLayout: {
type: Boolean,
value: false,
observer: 'onLayoutChange'
},
items: {
type: Array,
value: function() {
return ['Home', 'About', 'Pricing', 'Adults', 'Contact'];
}
},
routeData: Object,
subroute: String,
// This shouldn't be neccessary, but the Analyzer isn't picking up
// Polymer.Element#rootPath
// rootPath: String,
};
}
static get observers() {
return [
'_routePageChanged(routeData.page)',
];
}
_routePageChanged(page) {
// If no page was found in the route data, page will be an empty string.
// Default to 'view1' in that case.
this.page = page || 'about';
console.log('_routePageChange');
// Close a non-persistent drawer when the page & route are changed.
if (!this.$.drawer.persistent) {
this.$.drawer.close();
}
}
_pageChanged(page) {
// Load page import on demand. Show 404 page if fails
var resolvedPageUrl = this.resolveUrl(page + '.html');
Polymer.importHref(
resolvedPageUrl,
null,
this._showPage404.bind(this),
true);
}
_showPage404() {
this.page = 'view404';
}
_onLayoutChange(wide) {
var drawer = this.$.drawer;
if (wide && drawer.opened){
drawer.opened = false;
}
}
}
window.customElements.define(MyApp.is, MyApp);
</script>
Here's a snapshot of the page when I click on the "Home" link.
Snapshot of the page
I have fixed the same issue on my app, page observed functions like:
static get properties() { return {
page:{
type:String,
reflectToAttribute:true,
observer: '_pageChanged'},
...
_pageChanged(page, oldPage) {
if (page != null) {
if (page === "home" ) {
this.set('routeData.page', "");
} else {
this.set('routeData.page', page);
}
.......
}
}
Honestly, I am still trying to find the better solution. Because I have users page and I could not manage to able to indexed at google search results. This only keeps synchronized the iron-pages and address link.

Transition with keepScrollPosition and navigateBack

We are using Durandal for our SPA application and came to a, in my opinion, common use case. We have two pages: one page is a list of entities (with filters, sorting, virtual scroll) and another is detail preview of an entity. So, user is on list page and set a filter and a list of results comes out. After scrolling a little bit down user notice an entity which he/she would like to see details for. So clicking on a proper link user is navigated to details preview page.
After "work finished" on preview page user click back button (in app itself or browser) and he/she is back on the list page. However, default 'entrance' transition scroll the page to the top and not to the position on list where user pressed preview. So in order to 'read' list further user have to scroll down where he/she was before pressing preview.
So I started to create new transition which will for certain pages (like list-search pages) keep the scroll position and for other pages (like preview or edit pages) scroll to top on transition complete. And this was easy to do however, I was surprised when I noticed that there are strange behavior on preview pages when I hit navigateBack 'button'. My already long story short, after investigation I found out that windows.history.back is completing earlier then the transition is made and this cause that preview pages are scrolled automatically down to position of previous (list) page when back button is hit. This scrolling have a very unpleasant effect on UI not mentioning that it is 'total catastrophe' for my transition.
Any idea or suggestion what could I do in this case?
Here is the code of transition. It is just a working copy not finished yet as far as I have this problem.
define(['../system'], function (system) {
var fadeOutDuration = 100;
var scrollPositions = new Array();
var getScrollObjectFor = function (node) {
var elemObjs = scrollPositions.filter(function (ele) {
return ele.element === node;
});
if (elemObjs.length > 0)
return elemObjs[0];
else
return null;
};
var addScrollPositionFor = function (node) {
var elemObj = getScrollObjectFor(node);
if (elemObj) {
elemObj.scrollPosition = $(document).scrollTop();
}
else {
scrollPositions.push({element: node, scrollPosition: $(document).scrollTop()});
}
};
var scrollTransition = function (parent, newChild, settings) {
return system.defer(function (dfd) {
function endTransition() {
dfd.resolve();
}
function scrollIfNeeded() {
var elemObj = getScrollObjectFor(newChild);
if (elemObj)
{
$(document).scrollTop(elemObj.scrollPosition);
}
else {
$(document).scrollTop(0);
}
}
if (!newChild) {
if (settings.activeView) {
addScrollPositionFor(settings.activeView);
$(settings.activeView).fadeOut(fadeOutDuration, function () {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
});
} else {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
}
} else {
var $previousView = $(settings.activeView);
var duration = settings.duration || 500;
var fadeOnly = !!settings.fadeOnly;
function startTransition() {
if (settings.cacheViews) {
if (settings.composingNewView) {
ko.virtualElements.prepend(parent, newChild);
}
} else {
ko.virtualElements.emptyNode(parent);
ko.virtualElements.prepend(parent, newChild);
}
var startValues = {
marginLeft: fadeOnly ? '0' : '20px',
marginRight: fadeOnly ? '0' : '-20px',
opacity: 0,
display: 'block'
};
var endValues = {
marginRight: 0,
marginLeft: 0,
opacity: 1
};
$(newChild).css(startValues);
var animateOptions = {
duration: duration,
easing : 'swing',
complete: endTransition,
done: scrollIfNeeded
};
$(newChild).animate(endValues, animateOptions);
}
if ($previousView.length) {
addScrollPositionFor(settings.activeView);
$previousView.fadeOut(fadeOutDuration, startTransition);
} else {
startTransition();
}
}
}).promise();
};
return scrollTransition;
});
A simpler approach could be to store the scroll position when the module deactivates and restore the scroll on viewAttached.
You could store the positions in some global app variable:
app.scrollPositions = app.scrollPositions || {};
app.scrollPositions[system.getModuleId(this)] = theCurrentScrollPosition;

How can I dynamically change the visibility of a Durandal Route?

I have my Durandal routes configured as below.
var routes = [
....... More Routes Here.....
{
url: 'login',
moduleId: 'viewmodels/login',
name: 'Log In',
visible: true,
caption: 'Log In'
}, {
url: 'logout',
moduleId: 'viewmodels/logout',
name: 'Log Out',
visible: false,
caption: 'Log Out'
}, {
url: 'register',
moduleId: 'viewmodels/register',
name: 'Register',
visible: false,
caption: 'Register'
}];
And everything is working as expected. I would like to be able to activate the Logout Route in my navigation when I log in and my log in button to become invisible. I have tried the following code and despite not throwing any errors it does not change the visibility of anything in the interface.
var isLoggedIn = ko.observable(false);
isLoggedIn.subscribe(function (newValue) {
var routes = router.allRoutes();
if (newValue == true) {
for (var k = 0; k < routes.length; k++) {
if (routes[k].url == 'logout') {
routes[k].visible = true;
}
if (routes[k].url == 'login') {
routes[k].visible = false;
}
}
} else {
for (var i = 0; i < routes.length; i++) {
if (routes[i].url == 'logout') {
routes[i].visible = false;
}
if (routes[i].url == 'login') {
routes[i].visible = true;
}
}
}
});
I believe this doesn't work because visible is not an observable, isActive is a computed with no write capability so it does not work either. How can I dynamically change the visibility of my routes in the nav menu?
Here is what I ended up doing.
//ajax call to log user in
.done(function (recievedData) {
if (recievedData == true) {
router.deactivate();
return router.map(config.routesLoggedIn);
} else {
router.deactivate();
return router.map(config.routes);
}
}).done(function() {
router.activate('frames');
return router.navigateTo('#/frames');
});
Essentially created two routing profiles in my config object. One for logged in and one for not. There is one caveat. The router.deactivate() method is a very new method and is not in the NuGet package yet. I copied the code of the new router from the master branch of the GitHub repository for Durandal. There is some discussion on this new function on the Durandal User Group. Ultimately for security reasons I might feed the logged in routes from my server. But for the time being this should work just fine.
Another approach is to make all routes available but use bound expressions to compose either the content or the login page into the container view specified by the route.
Instead of supplying a literal view name, bind the compose parameter to a ternary expression that chooses between the name of the login view and the name of the content view. The controlling expression would be an observable such as app.isAuthenticated() the value of which must be set when the user succeeds in logging in or out.
This approach is robust in the face of deep linking because it does away with the notion of a path through the application. Without explicit redirection logic, it will authenticate the user and then show the requested resource.
It can be extended to more than two possible states using a function instead of a ternary expression. This is handy when different UI must be delivered according to user permission.