Angular ui-router with spa using sticky states not preserving scroll position on first back button - angular-ui-router

I'm creating an Angular mobile SPA app and part of the requirements have a substantial amount of list >> details pages. After they filter to populate the list, they can click on the item to go to a details page for it (with crud options if allowed, which rules out just using a modal). I'm saving the relevant data and things in a service, but this does not help maintain the scroll position and ui-router-extras sticky state seems like it would be ideal. I'm using requirejs as well, in case that's relevant. Also, I am using a Kendo ListView bound with a kendo.dataSource if that is relevant.
I've implemented it, but I'm not sure I've done it correctly. I scroll down and click on a list item and it goes to the details. When I click the back button (a directive that uses $window.history.back();, although I tried using $state.go and it did the same thing), it will go back and used the "cached" version, but will be at the top of the list. If I scroll down and click on another item and then go back, it will maintain the scroll position like I expect.
I turned on $stickyStateProvider.enableDebug(true); and the output looks identical from the first to the subsequent calls.
Here's my states config:
return app.config(function ($stateProvider, $stickyStateProvider, $urlRouterProvider) {
$stickyStateProvider.enableDebug(true);
$urlRouterProvider.otherwise(function ($injector, $location) {
var $state = $injector.get("$state");
$state.go("home");
});
$stateProvider.state('home', {
url: '/home',
templateUrl: 'app/views/home.html',
controller: 'homeController',
controllerAs: "vm"
})
.state('cmmenu', {
url: '/cmmenu',
templateUrl: 'app/views/cmmenu.html',
controller: 'cmmenuController',
controllerAs: "vm"
})
.state('areainquiry', {
//sticky: true,
abstract: true,
url: '/areainquiry',
templateUrl: 'app/views/areainquiry.html'
})
.state('areainquiry.list', {
url: '/areainquirylist',
views: {
'areainquirylist#areainquiry': {
templateUrl: 'app/views/areainquirylist.html',
controller: 'areainquirylistController',
controllerAs: "vm"
}
},
sticky: true,
deepStateRedirect: true
})
.state('areainquiry.details', {
url: '/areainquirydetails/:areaInquiryId',
views: {
'areainquirydetails#areainquiry': {
templateUrl: 'app/views/areainquirydetails.html',
controller: 'areainquirydetailsController',
controllerAs: "vm"
}
}
})
.state('login', {
url: '/login',
templateUrl: 'app/views/login.html',
controller: 'loginController',
controllerAs: "vm"
});
})
Here is the areainquiry.html that is the abstract parent view:
<div ui-view="areainquirylist" ng-show="$state.includes('areainquiry.list')"></div>
<div ui-view="areainquirydetails" ng-show="$state.includes('areainquiry.details')"></div>
Here is the code that is opening this screen. I do not have a named view in my index.html and all views up to areainquiry.html are using the root un-named view (I gave it a name "body" and changed all of them to use it, but it worked the same):
<div class="col-xs-6 col-md-4">
<a class="btn btn-default fullwidth" ui-sref="areainquiry.list">
<span class="{{ vm.areaInquiryButtonIcon }}"></span>
<br />
{{ vm.areaInquiryButtonText }}
</a>
</div>
I was trying to figure out a way to do it without the abstract state and just through a parent areainquiry and child areainquiry.detail, but couldn't get it working that way, so went this route.

Never mind. It is working. I'm not sure how I fixed it. I was working through another issue (had to do with a factory service), so I'm assuming that fixed it.

Related

Avoid fetching images everytime page load - vuejs

I'm building a page that show dynamically some photos in a feed like Instagram. I'm getting stuck trying to avoid everytime I load a page or I go into a photo's detail page and then go back, to do an API request to Laravel controller, so that means fetching data and images, losing the position of the page and starting on the top of the page.
My code:
Feed.vue
<template>
<div v-for="(image, index) in images" :key="index">
<v-img :src="image.path" class="image-masonry mini-cover" slot-scope="{ hover }"></v-img>
</div>
</template>
<script>
export default {
data() {
return {
images: []
}
},
mounted() {
this.getImagesHome();
},
methods: {
getImagesHome() {
this.axios.get('/api/images', {},
).then(response => {
this.images = response.data;
}).catch(error => {
console.log(error);
});
},
}
}
</script>
Edit:
I saw that keep-alive is primarily used to preserve component state or avoid re-rendering it. But i can't understand how to use it. I call my Feed.vue component in another Home.vue as below:
<template>
<v-app>
<Toolbar #toggle-drawer="$refs.drawer.drawer = !$refs.drawer.drawer"></Toolbar>
<Navbar ref="drawer"></Navbar>
<keep-alive>
<Feed></Feed>
</keep-alive>
</v-app>
</template>
<script>
import store from '../store';
export default {
components: {
'Toolbar' : () => import('./template/Toolbar.vue'),
'Navbar' : () => import('./template/Navbar.vue'),
'Feed' : () => import('./Feed.vue')
}
}
</script>
What i have to put more in keep-alive and what i have to change in my Feed.vue component?
mounted() should only be called once.
There seem to be multiple ways to go about this.
If you are using vue-router, then have a look at scrollBehaviour
https://router.vuejs.org/guide/advanced/scroll-behavior.html
From their documentation,
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return desired position
}
})
You can also try using keep-alive: https://v2.vuejs.org/v2/api/#keep-alive
It keeps the component in memory so it is not destroyed, you get activated and deactivated events to check when it comes into view.
But I don't think it saves scroll position, so you may want to use this in combination with scrollBehaviour

implementing angular-ui-router 'otherwise' state

If a user types myURL/ or myURL/#/ or even myURL/#/foo they get to my index page.
But if they type myURL/foo, they get a 404. This is terrible. They should instead be redirected to /.
I am trying to implement this and am not having a lot of luck.
(function() {
'use strict';
angular
.module('myApp')
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('index', {
name: 'index',
url: '/',
templateUrl: 'js/views/page1.html',
controllerAs: 'page1Controller',
data: { pageTitle: 'Main' }
})
.state('page2', {
name:'page2',
url: '/page2/:id',
templateUrl: 'js/views/page2.html',
controllerAs: 'page2Controller',
data: { pageTitle: 'page2' }
})
$urlRouterProvider.otherwise('/');
}]);
})();
I have looked at dozens of articles, and nowhere do I seem to be able find this simple case handled.
On the official docs it is mentioned that you can pass $injector and $location to the function otherwise.
Their example looks like this:
app.config(function($urlRouterProvider){
// if the path doesn't match any of the urls you configured
// otherwise will take care of routing the user to the specified url
$urlRouterProvider.otherwise('/index');
// Example of using function rule as param
$urlRouterProvider.otherwise(function($injector, $location){
... some advanced code...
});
})
What you can do to achieve your goal is to create a state, and whenever something it's not matched and enters otherwise fct, send it to that state.
$urlRouterProvider.otherwise(function($injector, $location){
$injector.get('$state').go('404');
});
I have not tested this but should work.

Vue 2 and child component events with multiple components

I've read this question / answer, and whilst that works when the parent contains a specific component, in my scenario the parent contains this:
<component :is="currentView"></component>
and the value of currentView determines which component is 'loaded' at any particular time. So, using v-on:event="handler" in the component tag here means all child components must use the same event name :(. Is it possible in the created() function of the parent to set up specific handlers which will be called regardless of which component is currently 'in view', and when they each might send different events? For example, I have a login component which sends a 'login' event, and a data view component which sends an 'update' event. In the parent, I want something like this:
this.$on('login', doLogin)
this.$on('update', doUpdate)
However, that's listening to events from itself, not its children. I also tried giving the component a ref:
<component ref="main" :is="currentView"></component>
and using this.$refs.main.$on('login', doLogin), but the $refs object isn't created until the initial render, so that can't go in the mounted() method (too early), and in the updated() method it will be repeatedly called which I'm sure isn't a good idea...
you could set a global event and then pass the name of the action as a part of the payload, i.e
const Login = {
template: `<h1>Login</h1>`,
mounted() {
this.$emit('global-event', {
action: 'login',
data: {
user: 'foo',
},
})
},
}
const Update = {
template: `<h1>Update</h1>`,
mounted() {
this.$emit('global-event', {
action: 'update',
data: {
user: 'foo bar',
},
})
},
}
new Vue({
el: '#app',
data: {
called: {
update: 0,
login: 0,
},
currentView: 'login',
},
components: {
Login,
Update,
},
methods: {
// within doSomething you would process the various events based on their payload.action
doSomething (payload) {
this.called[payload.action]++
},
toggleView () {
this.currentView = this.currentView === 'login' ? 'update' : 'login'
},
},
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<component #global-event="doSomething" :is="currentView"></component>
<button #click="toggleView">toggle view</button>
<pre>{{ called }}</pre>
</div>
Gah! After spending ages searching, finding nothing, then posting here, I of course immediately stumble on the answer.
But this only applies if you have control over the child components (I'm building all of them). In the child, you simply:
this.$parent.$emit('login', {some: 'data'})
And then set up the event listeners with this.$on('login', doLogin) in the parent as normal in the created() method.
I'm sure there will be a scenario where I'm using a third party component that simply calls this.$emit(), and at the moment, I can't see any alternative to v-on:eventName="handler" in the parent.

can't get nested views to work with ui-router

I have been able to switch out my angular router code with ui-router and it works fine with a basic setup. But on my page I have a partial that I want to reuse in multiple views and I can't get that working. Can anyone see an issue with my code below.
routes....
$stateProvider
.state( 'jobs', {
url: '/jobs',
templateUrl: templates.jobs,
views: {
'jobs': {
templateUrl: templates.jobList,
controller: 'JobListController'
},
'alert': {
templateUrl: templates.alert,
controller: 'AlertController'
}
}
})
templates.jobs file...
<div ui-view="jobs"></div>
<div ui-view="alert"></div>
and then the templates.jobList and templates.alert are just regular html blocks.
I have a main page with just a where my app initially loads. It's within this ui-view that I want to load the templates.jobs view along with it's nested views.
I've found that if I remove the templateUrl: templates.jobs from my jobs state and then move the two ui-views from that jobs file to my main html file it works. However, my main file needs to be able to load many potential views - the ui-view on the main html is just a placeholder where the rest of the application lives. So I can't have those two ui-views in my main file. Is there a way to make this work? Thanks.
Also set the parent view of jobs state in the views object and use absolute targeting:
$stateProvider.state('jobs', {
url: '/jobs',
views: {
'#': {
template: '<div ui-view="jobs"></div><div ui-view="alert"></div>',
controller: function () {}
},
'jobs#jobs': {
template: '<h2>Joblist</h2>',
controller: function () {}
},
'alert#jobs': {
template: '<h2>alert</h2>',
controller: function () {}
}
}
});
Working example on Plunker: http://plnkr.co/edit/AR8RK1Ik1xeGL0xUBV6e?p=preview
Reference: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views#view-names---relative-vs-absolute-names

BackboneJS + Codeigniter pushState true not working

So I have this Backbone App where I use Codeigniter for the Backend. For some reason, pushState:true does not work.
So, my main.js of my backbone app has this:
Backbone.history.start({ pushState: true, root: App.ROOT });
My app.js has this:
var App = {
ROOT: '/projects/mdk/'
};
and my navigation module, which renders the menulinks, each item has this:
this.insertView(new ItemView({
model: new Navigation.ItemModel({
href: App.ROOT + 'home',
class: 'home',
triggers: 'home',
route: this.route
})
}));
and the model for it:
Navigation.ItemModel = Backbone.Model.extend({
defaults: {
href: '',
text: '',
triggers: [],
route: ''
}
});
All I get from this is "Page not found"...
Add: When I in the view change it to href:'#news' - it works, but it dont really makes sense...
Anyone who knows the issue here?
From the documentation (http://backbonejs.org/#History):
Note that using real URLs requires your web server to be able to
correctly render those pages, so back-end changes are required as
well. For example, if you have a route of /documents/100, your web
server must be able to serve that page, if the browser visits that URL
directly.
The problem is that your server isn't responding to whatever URL your app is on. For every URL that your Backbone app can reach, your server MUST return a valid HTML page (contianing your Backbone app).
ok I found a solution by myself:
I made this hack:
$(document).on('click', 'a:not([data-bypass])', function (evt) {
var href = $(this).attr('href');
if (href && href.indexOf('#') === 0) {
evt.preventDefault();
Backbone.history.navigate(href, true);
}
});
and then I made:
href: '#home',
That solved the problem, now evereythings runs fluently..

Resources