Aurelia: How to access data stored in a child-router's view-model - aurelia

I am fairly new to Aurelia and I am trying to understand what’s the best way to access and display data in a subpage of a child-router. The data is stored in the activate method of the child-router’s view-model. My Problem is to display the data when I first enter or reload a subpage of the child-router. Unfortunately it's not working. As soon as I have displayed a subpage and go to another, it all works fine.
child-router.js
export class ChildRouter {
configureRouter(config, router) {
config.title = 'Child Router Title';
config.map([
{ route: '', redirect: 'basics'},
{ route: ['basics', ''], name: 'basics', moduleId: './basics/basics', nav: true, title: 'Basics' },
{ route: 'data', name: 'data', moduleId: './data/data', nav: true, title: 'Data' }
]);
this.router = router;
}
activate() {
this.var1 = "Var1. Only works when I reenter a subpage.";
this.var2 = "Var2. Only works when I reenter a subpage.";
}
}
child-router.html
<template>
<require from="../components/detail-navigation.html"></require>
<h2>${router.title}</h2>
<detail-navigation router.bind="router"></detail-navigation>
<div class="page-host">
<router-view></router-view>
</div>
</template>
basics.html
<template>
<h2>Basics Title</h2>
<h3>${var1}</h3>
</template>
data.html
<template>
<h2>Data Title</h2>
<h3>${var2}</h3>
</template>
I hope you understand my problem.
Here is a link to a test projekt on git.
I am looking forward for any recommendations.

Personally, I would really recommend you not try to do this. This is introducing tight coupling between the ChildRouter page and any pages displayed as routes on it. If you need these pages to talk to each other, consider using the Dependency Injection provider to inject an instance of the same class in to each page and sharing information that way.

Related

Dynamic Vue Router

I am researching whether a vue router is the best approach for the following scenario:
I have a page containing 'n' number of divs. Each of the divs have different content inside them. When a user clicks on a button in the div, I would like the div to open in a separate browser window (including its contents).
Can a route name/component be created on the fly to route to? Since I have 'n' number of divs, that are created dynamically, I cannot hard-code name/components for each one
<router-link :to="{ name: 'fooRoute'}" target="_blank">
Link Text
</router-link>
I want to avoid the same component instance being used (via route with params) since I may want multiple divs to be open at the same time (each one in their own browser window)
If the link is opening in a separate window, it makes no sense to use a <router-link> component as the application will load from scratch in any case. You can use an anchor element instead and generate the href property dynamically for each div.
To answer your questions:
A route name cannot be created dynamically since all routes must be defined at the beginning, when the app (along with router) is being initialized. That said, you can have a dynamic route and then dynamically generate different paths that will be matched by that route.
There is no way for the same component instance to be reused if it's running in a separate browser window/tab.
It is possible to create dynamic router name.
profileList.vue
<template>
<main>
<b-container>
<b-card
v-for="username in ['a', 'b']"
:key="username"
>
<b-link :to="{ name: profileType + 'Profile', params: { [profileType + 'name']: username }}">Details</b-link>
</b-container>
</main>
</template>
<script>
export default {
name: 'profileList',
data () {
return {
profileType: ''
}
},
watch: {
// Call again the method if the route changes.
'$route': function () {
this.whatPageLoaded()
}
},
created () {
this.whatPageLoaded()
},
methods: {
whatPageLoaded () {
this.profileType = this.$route.path // /user or /place
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
</style>
b-container, b-card, b-link are taken from bootstrap-vue, so you can freely change it.
router.js
const router = new Router({
mode: 'hash',
base: process.env.BASE_URL,
linkExactActiveClass: 'active',
routes: [
// USERS
{
path: '/user/:username',
name: userProfile,
component: userProfile
},
{
path: '/user',
name: 'userList',
component: profileList
},
// PLACES
{
path: '/place/:placename',
name: placeProfile,
component: placeProfile
},
{
path: '/place',
name: 'placeList',
component: ProfileList
}
]
})

Accessing another model-view from a page inside the main router

I’m trying to find out a way to creating a custom view-model and making its functions accessible from a page within the main router-view element. This custom view-model, in my case called "secondbar" is supposed to be located under main nav-bar and should contain a login status ("Logged in as ..." / "Not logged in").
In my router-view, one of the pages is a login page. After successful login, I want to be able to call a function of "secondbar" directly in order to change the login status there without page refresh.
I tried to inject "secondbar" class in login.js file; this way I can access the functions, but the message on the page wouldn't change (it seems like I'm accessing another instance of "secondbar"). I also tried to print out the same message directly on the main nav-bar, but it seems like this is not the right approach and it didn’t work either.
Is there some way, how I can access a "secondbar" class directly (the same instance is being shown in the browser) and call a function located there from a page inside a router-view?
App.html
<template bindable="router">
<require from="secondbar/secondbar"></require>
<!-- navbar -->
<secondbar view-model.ref="secondbar"></secondbar>
<router-view>
<!—- page content -->
</router-view>
</template>
App.js
import {Redirect} from 'aurelia-router';
import {inject} from 'aurelia-framework';
export class App {
configureRouter(config, router) {
this.router = router;
config.title = ‘’;
config.map([
{ route: [''], name: 'home', moduleId: 'home/index', nav: true, title: 'Home' , settings: { roles: [''] }},
{ route: 'login', name: 'login', moduleId: 'login/login', nav: true, title: 'Log In', settings: { roles: [''] }},
]);
}
}
What you could do is create a login-service or something similar, where you can bind to in both the secondbar and the login page-view. The basic would look like this:
login-page:
import login-service from "/login-service/login-service";
// inject
login() {
// do login stuff
this.loginService.currentUser = user; // user would be any object of choice
}
secondbar.js:
import login-service from "/login-service/login-service";
// inject
secondbar.html
<span if.bind="loginService.currentUser">
Logged in as: ${loginService.currentUser.username}
</span>
<span if.bind="!loginService.currentUser">
Not logged in
</span>

How can Aurelia child routing work with a parameter?

I feel as though similar questions have already been asked, but I have been unable to find my answer.
I'm trying to segregate my application by its features. Ideally each feature would be able to setup its own routing as well and Aurelia's child-router functionality seemed to be the perfect fit, but I'm having trouble getting it to work.
The structure of the application is as such:
app.ts
app.html
/lectures
list.ts
list.html
details.ts
details.html
index.ts
index.html
I can include any of the other files if needed to answer the question, but have tried to keep the question as compact as possible. The app.html and lectures/index.html files both only contain <template><router-outlet></router-outlet></template>.
I have app.ts:
import { Router, RouterConfiguration } from 'aurelia-router';
export class App {
configureRouter(config: RouterConfiguration, router: Router) {
config.options.pushState = true;
config.map([
{
moduleId: './public-site/lectures',
name: 'lectures',
nav: true,
route: ['', 'lectures/:id?'],
title: 'Lectures'
}
]);
}
}
lecture/index.ts
import { Router, RouterConfiguration } from 'aurelia-router';
export class Index {
configureRouter(config: RouterConfiguration, router: Router) {
config.options.pushState = true;
config.map([
{ route: '', moduleId: './list' },
{ route: ':id', moduleId: './details' }
]);
}
}
and then I have lectures/details.ts
import { NavigationInstruction, RouteConfig, RoutableComponentActivate } from 'aurelia-router';
export class LectureDetails implements RoutableComponentActivate {
activate(params: any, routeConfig: RouteConfig, navigationInstruction: NavigationInstruction): Promise<any> {
debugger;
}
}
and lecture\list.html
<template>
<div repeat.for="lecture of lectures" class="grid-body-cell" click.delegate="navigateToLecture(lecture)">
${lecture.title}
</div>
</template>
lecture\list.ts
import { autoinject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
#autoinject()
export class LecturesList {
constructor(private router: Router) {}
navigateToLecture(lecture: {id:number}) {
this.router.navigate(`#/lectures/${lecture.id}`);
}
}
When the app loads, it correctly navigates and displays the list page, but when I click on any of the lectures in the grid, the url updates to /lectures/1, but my debugger statement never gets hit. I can't figure out what I'm doing wrong.
What seems to be happening is that, while the url gets updated, the router is still directing the application to the list component. Is there a way to get the router to honor and pass on the parameter to the child router?
How do I need to update my setup to get the child router to work with the parameter?
Firstly, the router element for displaying routed views is called <router-view> so your views for child routers should be: <template><router-view></router-view></template> - I believe <router-outlet> is what you use in Angular 2+ applications for routing. I am making the assumption here you are already doing that if you're seeing things being rendered.
Secondly, you have config.options.pushState = true defined on your root router configuration and then inside of your navigateToLecture method you are passing in a hash (which is what you would do if you're not using pushState). So Aurelia is removing the hash from the URL (as intended) because you're using pushState and pushState URL's don't need to use the # hack.
Thirdly, I would name your routes (and you'll discover why in a moment). Naming your routes allows you to reference them by name and use either navigateToRoute('routename', {paramsobject}) or route-href (which we discuss below).
So, in lecture/index.ts, put a name property on your routes:
import { Router, RouterConfiguration } from 'aurelia-router';
export class Index {
configureRouter(config: RouterConfiguration, router: Router) {
config.options.pushState = true;
config.map([
{ route: '', name: 'lecture-list', moduleId: './list' },
{ route: ':id', name: 'lecture-detail', moduleId: './details' }
]);
}
}
And lastly, instead of having a click event in your view which is calling router.navigate, you can use the route-href attribute which will allow you to make links work with the router. So, something like the following:
<template>
<div repeat.for="lecture of lectures" class="grid-body-cell">
<a route-href="route: lecture-detail; params.bind: { id: lecture.id }">${lecture.title}</a>
</div>
</template>
Notice how we are referencing our newly named route by its name, lecture-detail? Now Aurelia will come through and parse our link and update the href property to go where it needs too.
Hope that helps.
app.html
<template><router-view></router-view></template>
app.ts
import { Router, RouterConfiguration } from 'aurelia-router';
export class App {
configureRouter(config: RouterConfiguration, router: Router) {
config.pushState = true;
config.map([
{
moduleId: './lectures/index',
name: 'lectures',
nav: true,
route: ['', 'lectures/*id'],
title: 'Lectures'
}
]);
};
}
lectures/index.html
<template><router-view></router-view></template>
lectures/list.html
<template>
<div repeat.for="lecture of lectures" class="grid-body-cell" click.delegate="navigateToLecture(lecture)">
${lecture.title}
</div>
</template>
lectures/list.ts
import { autoinject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
#autoinject()
export class LecturesList {
constructor(private router: Router) {}
navigateToLecture(lecture: {id:number}) {
this.router.navigateToRoute(`lectures`, { id: lecture.id });
}
}
This took me hours of banging my head against the wall and I'm not even sure this will continue to work when I add more routes to the lectures/index.ts, but it works for now and allows me to continue on.

How to install/setup Aurelia router/routes

I want a few links at the top of my page, and when they are clicked, different views are presented to the user on the same page. I want to set this up from scratch.
I've seen a few examples online of this working but when I try to set it up from scratch using the Arelia todo example as a baseline (http://aurelia.io/hub.html#/doc/article/aurelia/framework/latest/quick-start), I seem to missing something. I assume I need to install the Aurelia router but there are no instruction anywhere to do this ( that I can find). The read me at the Github page does not give instructions on how to install it.
What I know.
I will need an app.js file that has the routes and looks something like this:
export class App {
configureRouter(config, router) {
config.title = 'Aurelia';
config.map([
{route: ['','home'], name: 'home', moduleId: 'components/home/home', nav: true, title: 'Home'},
{route: ['settings'], name: 'settings', moduleId: '/settings/settings', nav: true, title: 'Settings'}
]);
this.router = router;
}
}
I will need an app.html file that looks something like this ( this loops through the objects in the previous code and accesses their properties).
<template>
<nav>
<ul>
<li repeat.for="row of router.navigation"> <!--Loop through routes-->
<a href.bind="row.href">${row.title}</a>
</li>
</ul>
</nav>
<router-view></router-view>
<hr>
</template>
The results is a blank page with no errors. Any static HTML I place on app.html will render but other than that - nothing.
If you're following on from the example from the Aurelia website, then you will have noticed that the main.js in their tutorial is
export function configure(aurelia) {
aurelia.use.basicConfiguration();
aurelia.start().then(() => aurelia.setRoot());
}
Change this to
export function configure(aurelia) {
aurelia.use.standardConfiguration();
aurelia.start().then(() => aurelia.setRoot());
}
In my experience with the quick starter, I had to also add into the index.html the "aurelia-routing.min.js". So my index.html looks like:
<body aurelia-app="src/main">
<script src="scripts/system.js"></script>
<script src="scripts/config-typescript.js"></script>
<script src="scripts/aurelia-core.min.js"></script>
<script src="scripts/aurelia-routing.min.js"></script>
<script>
System.import('aurelia-bootstrapper');
</script>
</body>
If however you're starting to get more into Aurelia, I suggest you start with their next tutorial

Hiding routes in Aurelia nav bar until authenticated

Is there a proper way to hide items in the Aurelia getting started app behind some authentication.
Right now I'm just adding a class to each element based on a custom property. This feels extremely hacky.
<li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}${!row.isVisible ? 'navbar-hidden' : ''}">
<a href.bind="row.href">${row.title}</a>
</li>
There are two directions you can take here.
The first is to only show nav links in the nav bar when the custom property is set like you are. To clean it up a bit let's use the show binding -
<li repeat.for="row of router.navigation" show.bind="isVisible" class="${row.isActive ? 'active' : ''}">
<a href.bind="row.href">${row.title}</a>
</li>
The issue here is you still need to maintain the custom property like you are already doing. The alternative is to reset the router. This basically involves building out a set of routes that are available when the user is unauthenticated and then a separate set once the user is authenticated -
this.router.configure(unauthenticatedRoutes);
// user authenticates
this.router.reset();
this.router.configure(authenticatedRoutes);
This gives you the flexibility to reconfigure the router whenever you need to.
These answers are great, though for the purposes of authentication, I don't think any have the security properties you want. For example, if you have a route /#/topsecret, hiding it will keep it out of the navbar but will not prevent a user from typing it in the URL.
Though it's technically a bit off topic, I think a much better practice is to use multiple shells as detailed in this answer: How to render different view structures in Aurelia?
The basic idea is to send the user to a login application on app startup, and then send them to the main app on login.
main.js
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
// notice that we are setting root to 'login'
aurelia.start().then(app => app.setRoot('login'));
}
app.js
import { inject, Aurelia } from 'aurelia-framework';
#inject(Aurelia)
export class Login {
constructor(aurelia) {
this.aurelia = aurelia;
}
goToApp() {
this.aurelia.setRoot('app');
}
}
I've also written up an in-depth blog with examples on how to do this: http://davismj.me/blog/aurelia-login-best-practices-pt-1/
Although I like PW Kad's solution (it just seems cleaner), here's an approach that I took using a custom valueConvertor:
nav-bar.html
<ul class="nav navbar-nav">
<li repeat.for="row of router.navigation | authFilter: isLoggedIn" class="${row.isActive ? 'active' : ''}" >
<a data-toggle="collapse" data-target="#bs-example-navbar-collapse-1.in" href.bind="row.href">${row.title}</a>
</li>
</ul>
nav-bar.js
import { bindable, inject, computedFrom} from 'aurelia-framework';
import {UserInfo} from './models/userInfo';
#inject(UserInfo)
export class NavBar {
#bindable router = null;
constructor(userInfo){
this.userInfo = userInfo;
}
get isLoggedIn(){
//userInfo is an object that is updated on authentication
return this.userInfo.isLoggedIn;
}
}
authFilter.js
export class AuthFilterValueConverter {
toView(routes, isLoggedIn){
console.log(isLoggedIn);
if(isLoggedIn)
return routes;
return routes.filter(r => !r.config.auth);
}
}
Note the following:
Your isLoggedIn getter will be polled incessantly
You can achieve the same with an if.bind="!row.config.auth || $parent.isLoggedIn" binding, but make sure that your if.bind binding comes after your repeat.for
I realize this is a bit of thread necromancy, but I wanted to add an answer because the accepted answer offers a solution that's explicitly recommended against by the Aurelia docs (you have to scroll down to the reset() method.
I tried several other methods, to varying degrees of success before I realized that I was looking at it wrong. Restriction of routes is a concern of the application, and so using the AuthorizeStep approach is definitely the way to go for blocking someone from going to a given route. Filtering out which routes a user sees on the navbar, though, is a viewmodel concern in my opinion. I didn't really feel like it was a value converter like #MickJuice did, though, as every example I saw of those were about formatting, not filtering, and also I felt like it's a bit cleaner / more intuitive to put it in the nav-bar view model. My approach was as follows:
// app.js
import AuthenticationService from './services/authentication';
import { inject } from 'aurelia-framework';
import { Redirect } from 'aurelia-router';
#inject(AuthenticationService)
export class App {
constructor(auth) {
this.auth = auth;
}
configureRouter(config, router) {
config.title = 'RPSLS';
const step = new AuthenticatedStep(this.auth);
config.addAuthorizeStep(step);
config.map([
{ route: ['', 'welcome'], name: 'welcome', moduleId: './welcome', nav: true, title: 'Welcome' },
{ route: 'teams', name: 'teams', moduleId: './my-teams', nav: true, title: 'Teams', settings: { auth: true } },
{ route: 'login', name: 'login', moduleId: './login', nav: false, title: 'Login' },
]);
this.router = router;
}
}
class AuthenticatedStep {
constructor(auth) {
this.auth = auth;
}
run(navigationInstruction, next) {
if (navigationInstruction.getAllInstructions().some(i => i.config.settings.auth)) {
if (!this.auth.currentUser) {
return next.cancel(new Redirect('login'));
}
}
return next();
}
}
OK, so that by itself will restrict user access to routes if the user isn't logged in. I could easily extend that to something roles based, but I don't need to at this point. The nav-bar.html then is right out of the skeleton, but rather than binding the router directly in nav-bar.html I created nav-bar.js to use a full view-model, like so:
import { inject, bindable } from 'aurelia-framework';
import AuthenticationService from './services/authentication';
#inject(AuthenticationService)
export class NavBar {
#bindable router = null;
constructor(auth) {
this.auth = auth;
}
get routes() {
if (this.auth.currentUser) {
return this.router.navigation;
}
return this.router.navigation.filter(r => !r.settings.auth);
}
}
Rather than iterating over router.navigation at this point, nav-bar.html will iterate over the routes property I declared above:
<ul class="nav navbar-nav">
<li repeat.for="row of routes" class="${row.isActive ? 'active' : ''}">
<a data-toggle="collapse" data-target="#skeleton-navigation-navbar-collapse.in" href.bind="row.href">${row.title}</a>
</li>
</ul>
Again, your mileage may vary, but I wanted to post this as I thought it was a fairly clean and painless solution to a common requirement.