I have implemented a splash screen in my app, as this way:
index.html:
<app-root></app-root>
<div class="splash spinner"></div>
css:
// ... Styles about spinner
app-root:empty + .splash {
opacity: 1;
}
Ok, in this case, the app-root when it's empty, I have a spinner animation and there is no problem, it works fine.
But, my problem comes now, I have lazy loading in routing:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { AuthGuard } from './core';
import {
...
} from './auth';
export const routes: Routes = [
{
..
},
{
path: 'api',
loadChildren: 'app/api/myapi.module#MyApiModule',
canActivate: [ AuthGuard ],
},
},
];
#NgModule({
imports: [
RouterModule.forRoot(routes, {
enableTracing: true
}),
],
exports: [ RouterModule ]
})
export class AppRoutingModule { }
In /apis path I'm lazy loading MyApiModule, and inside MyApiModule, I have another routing:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { MyComponent } from './mycomponent.component';
import { OtherComponent } from './othercomponent.component';
const routes: Routes = [
{
path: '',
component: MyComponent,
children: [
{
path: '',
component: OtherComponent,
},
{
path: ':id/api',
loadChildren: 'app/api/api2/api.module#ApiModule',
},
],
},
];
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class OtherRoutingModule { }
I want to show the same splash screen as I set in 'app-root' when my app is loading a 'lazy-loaded' module (the ApiModule module).
The goal is to show splash screen when a lazy module is loading so, my question is, is there any 'easy' way to catch when a load-children module is loaded (by events or something)? Or I need to accomplish the same way as I did (check in css the router-outlet... etc etc).
app-root:empty selector is more of a hack rather than universal solution for loading indicator. This or similar selector can still be additionally used for initially rendered page when scripts aren't loaded yet.
Loading indicator may be shown in some common scenarios:
Asynchronous module-level lazy loading (SystemJS, Webpack chunk loading, etc)
long synchronous application-level actions
asynchronous application-level actions
HTTP requests (Http, HttpClient, native XHR, ...)
IndexedDB requests
web worker interoperation
All of these scenarios can occur independently (e.g. triggered by components) or be applicable to Angular application phases that may cause visible delays:
initial application initialization
route change
It always depends on the case whether a spinner should be triggered at lower (scenarios) or higher (phases) level.
Some scenarios can be conventionally handled (HttpClient requests can be tracked with interceptors).
Some scenarios cannot be efficiently handled. This includes ES and Angular lazy-loaded modules, since they are handled at low level and usually don't expose a promise to chain.
So lazy-loaded Angular modules leave the only option to trigger a spinner, i.e. on route change:
router.events.subscribe(e => {
if (e instanceof NavigationStart) {
// show spinner
} elseif (e instanceof NavigationEnd {
|| e instanceof NavigationError
|| e instanceof NavigationCancel) {
// hide spinner
}
});
If some of asynchronous scenarios occur outside of route resolvers, HttpClient interceptors can be additionally involved.
Since several processes that trigger a spinner may occur simultaneously, a spinner should be implemented with a counter and not boolean flag, like is shown here.
Related
I have a simple Loader Module hosted in a private npm repository. This module only intercepts http calls and displays a css loader. The code is simple: it has a public api that exports the module and a component. The module has an http interceptor that calls a service which sets a variable isLoading.
The component code is split into three files, as usual (ts, html and scss).
loader.component.ts
import { Component, OnInit, AfterViewInit } from '#angular/core';
import { LoaderService } from '../service/loader.service';
#Component({
selector: 'loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.scss']
})
export class LoaderComponent implements OnInit {
public isLoading: boolean;
constructor(private loaderService: LoaderService) { }
ngOnInit(){
this.loaderService.isLoading.subscribe(status => this.isLoading = status);
}
}
loader.component.html
<div *ngIf="isLoading" class="overlay">
<div class="loader center-block"></div>
<p class="wait">
<strong>Wait... </strong>
</p>
</div>
The loader.component.scss file defines the loader, overlay and wait css classes (not shown for brevity).
The interceptor code is here:
#Injectable()
export class LoaderInterceptor implements HttpInterceptor {
constructor(private loaderService: LoaderService) {}
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<any>> {
this.loaderService.show();
return next.handle(request).pipe(finalize(() => this.loaderService.hide()));
}
}
And finally, the service code:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class LoaderService {
public isLoading: Subject<boolean>;
constructor() {
this.isLoading = new Subject<boolean>();
}
show() {
this.isLoading.next(true);
}
hide() {
this.isLoading.next(false);
}
}
This module is imported at the app.module.ts of an application and declared in the imports array of the decorator.
The application has some routes with lazy load feature modules.
The loader module is used at the app.component.html file, as follows:
<loader></loader>
<router-outlet></router-outlet>
The problem: the loader does not work the first time we visit a route, but it works if we click the previous button at the browser or we click to go back to homepage or any other way I tested visiting a route twice or more. Otherwise, it doesn't work.
I tried to find similar questions, I tried to see the order of css imports, I also tried to analise the loader module, the import order, but no clue about what is happening.
Using #angular/router": "3.4.7".
Proposed solution here doesn't work anymore.
/**
The ProductComponent depending on the url displays one of two info
components rendered in a named outlet called 'productOutlet'.
*/
#Component({
selector: 'product',
template:
` <router-outlet></router-outlet>
<router-outlet name="productOutlet"></router-outlet>
`
})
export class ProductComponent{
}
#NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: 'product',
component: ProductComponent,
children: [
{
path: '',
component: ProductOverviewComponent,
outlet: 'productOutlet'},
{
path: 'details',
component: ProductDetailsComponent,
outlet: 'productOutlet' }
]
}
]
)],
declarations: [
ProductComponent,
ProductHeaderComponent,
ProductOverviewComponent,
ProductDetailsComponent
exports: [
ProductComponent,
ProductHeaderComponent,
ProductOverviewComponent,
ProductDetailsComponent
]
})
export class ProductModule {
}
Manual navigation works as expected
http://localhost:5000/app/build/#/product-module/product (correctly displays overview component in named outlet)
http://localhost:5000/app/build/#/product-module/product/(productOutlet:details)
(correctly displays details component in named outlet)
THE PROBLEM
Cannot figure out the correct way to perform programatical navigation:
this.router.navigateByUrl('/(productOutlet:details)');
this.router.navigate(['', { outlets: { productOutlet: 'details' } }]);
Following errors occur:
Error: Cannot match any routes. URL Segment: 'details'.
You can navigate programatically like this
this.router.navigate([{ outlets: { outletName: ['navigatingPath'] } }], { skipLocationChange: true });
Note: skipLocationChange is use to hide the url from the address bar.
Refer the official document : https://angular.io/guide/router
You can try relative navigation as
this.router.navigate(['new'],{relativeTo:this.route})
For this you will have to inject current router snapshot and Activated route in component as
import { Router,ActivatedRoute } from "#angular/router";
constructor(private router:Router,private route:ActivatedRoute ){}
I'm following this instructional video, Building web apps powered by Angular 2.x using Visual Studio 2017, and around 51:00 is the part I'm at and I'm hitting a problem in this source file:
https://github.com/CRANK211/vs17-ng2-dnc/blob/master/3%20-%20with-data/components/shared/account.service.ts#L18
With this function:
getAccountSummaries() {
return this.http.get('api/Bank/GetAccountSummaries')
.map(response => response.json() as AccountSummary[])
.toPromise();
}
I'm getting red text in Visual Studio on .json() which says
Symbol 'json' cannot be properly resolved, probably because it is located in inaccessible module
and when I try to run the application I get the exception message:
System.Exception: Call to Node module failed with error: Error: Uncaught (in promise): Error: No provider for AccountService!
Following the tutorial I used the same template as the instructor did but I think something must have changed since then since he has a single app.module.ts while my template came with four: app.module.client.ts, app.module.server.ts, and app.module.shared.ts and unfortunately as someone new to ASP.NET Core and Angular2 I have no idea why they're different or what the significance might be.
I've had success up to now by just making any changes he makes to his app.module.ts to my app.module.shared.ts which you can see here:
import { NgModule } from '#angular/core';
import { RouterModule } from '#angular/router';
import { AppComponent } from './components/app/app.component'
import { NavMenuComponent } from './components/navmenu/navmenu.component';
import { HomeComponent } from './components/home/home.component';
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
import { CounterComponent } from './components/counter/counter.component';
import { HeaderComponent } from './components/shared/header/header.component';
import { AccountListComponent } from './components/account/account-list/account-list.component';
import { AccountSummaryComponent } from './components/account/account-summary/account-summary.component';
import { AccountDetailComponent } from './components/account/account-detail/account-detail.component';
import { FormatAccountNumberPipe } from './components/shared/format-account-number.pipe';
import { AccountActivityComponent } from './components/account/acccount-activity/account-activity.component';
import { AccountService } from './components/shared/account.service';
export const sharedConfig: NgModule = {
bootstrap: [ AppComponent ],
declarations: [
AppComponent,
NavMenuComponent,
CounterComponent,
FetchDataComponent,
HomeComponent,
HeaderComponent,
AccountListComponent,
AccountDetailComponent,
AccountSummaryComponent,
AccountActivityComponent,
FormatAccountNumberPipe
],
imports: [
RouterModule.forRoot([
{ path: '', redirectTo: 'account', pathMatch: 'full' },
{ path: 'account', component: AccountListComponent },
{ path: 'detail/:id', component: AccountDetailComponent },
{ path: '**', redirectTo: 'account' }
])
],
providers: [ AccountService ]
};
Everything compiled fine and worked just like his until this .json() line unfortunately.
How do I fix it?
The red text you get from Visual Studio is probably because it VS cannot resolve the response object. The error should be gone when you prepend the following to your file
import { Response } from '#angular/http';
and change add the type Response to your map functions like so:
getAccountSummaries() {
return this.http.get('/api/Bank/GetAccountSummaries')
.map((response: Response) => response.json() as AccountSummary[])
.toPromise();
}
The other issue you have with the missing provider, is probably because the AccountService is used in a component, and this component is part of a module, and this module does not have the AccountService defined as a Provider. So make sure that every module you have has
providers:[ AccountService ]
defined in it's configuration.
hope that helps
I'd like to implement the integration testing of my Relay containers against a running GraphQL backend server. I'm going to use Jest for this. I'd like to say that unit testing of React components works well as expected with my Jest setup.
Here's what I have in the package.json for the Jest:
"jest": {
"moduleFileExtensions": [
"js",
"jsx"
],
"moduleDirectories": [
"node_modules",
"src"
],
"moduleNameMapper": {
"^.+\\.(css|less)$": "<rootDir>/src/styleMock.js",
"^.+\\.(gif|ttf|eot|svg|png)$": "<rootDir>/src/fileMock.js"
},
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react/",
"<rootDir>/node_modules/react-dom/",
"<rootDir>/node_modules/react-addons-test-utils/",
"<rootDir>/node_modules/react-relay/"
]
}
Here's the .babelrc I'm using:
{
"presets": ["es2015", "react", "stage-0"],
"plugins": ["./babelRelayPlugin.js"]
}
Here's the test itself. It must make a request to `http://localhost:10000/q' GraphQL endpoint to fetch a simple piece that represents the info about the current user ('me').
jest.disableAutomock();
import React from 'react';
import Relay from 'react-relay';
import TestUtils from 'react-addons-test-utils';
import RelayNetworkDebug from 'react-relay/lib/RelayNetworkDebug';
RelayNetworkDebug.init();
Relay.injectNetworkLayer(
new Relay.DefaultNetworkLayer('http://localhost:10000/q')
);
describe('Me', () => {
it('can make request to /q anyway', () => {
class RootRoute extends Relay.Route {
static queries = {
root: (Component) => Relay.QL`
query {
root {
${Component.getFragment('root')}
}
}
`,
};
static routeName = 'RootRoute';
}
class AppRoot extends React.Component {
static propTypes = {
root: React.PropTypes.object,
};
render() {
expect(this.props.root).not.toBe(null);
expect(this.props.root.me).not.toBe(null);
expect(this.props.root.me.firstName).not.toBe(null);
expect(this.props.root.me.authorities[0]).not.toBe(null);
expect(this.props.root.me.authorities[0].authority).toEqual('ROLE_ANONYMOUS_AAA');
return (
<div>
{this.props.root.me.firstName}
</div>
);
}
}
const AppContainer = Relay.createContainer(AppRoot, {
fragments: {
root: () => Relay.QL`
fragment on Root {
me {
firstName
email
authorities {
authority
}
}
}
`,
},
});
const container = TestUtils.renderIntoDocument(
<div>
<Relay.RootContainer Component={AppContainer} route={new RootRoute()} />
</div>
);
expect(container).not.toBe(null);
});
});
The problem is that the test passes. But in my opinion it must fail at this line inside the render() expect(this.props.root.me.authorities[0].authority).toEqual('ROLE_ANONYMOUS_AAA');. It seems like the render() method is not executed at all.
I'm running Jest like this
./node_modules/.bin/jest
Does this all suppose to work at all?
Thank you.
This is possible, take a look on the code: https://github.com/sibelius/relay-integration-test
and on my blog post: https://medium.com/entria/relay-integration-test-with-jest-71236fb36d44#.ghhvvbbvl
The missing piece is that you need to polyfill XMLHttpRequest to make it work with React Native.
And you need to polyfill fetch for React web
(PS: I'm using Meteor + React + React Router. The application structure is not traditional, I'm making a package-esq application, an example is https://github.com/TelescopeJS/Telescope. I'm trying to do dynamic routing with react router and things are not working out well.)
There be something wrong with browserHistory. Navigation refreshes the page. Going back and forth through the browser buttons refreshes the page.
Example of this, with all codes, are here - https://github.com/dbx834/sandbox
React-Router specific codes follow,
In a core package, with a global export, allow registeration of routes and components
...
// ------------------------------------- Components -------------------------------- //
Sandbox.components = {};
Sandbox.registerComponent = (name, component) => {
Sandbox.components[name] = component;
};
Sandbox.getComponent = (name) => {
return Sandbox.components[name];
};
// ------------------------------------- Routes -------------------------------- //
Sandbox.routes = {};
Sandbox.routes.routes = [];
Sandbox.routes = {
routes: [],
add(routeOrRouteArray) {
const addedRoutes = Array.isArray(routeOrRouteArray) ? routeOrRouteArray : [routeOrRouteArray];
this.routes = this.routes.concat(addedRoutes);
},
};
...
In various implementations (domain specific logic, UI, etc), register components and routes
...
import TodoApp from './components/TodoApp.jsx';
Sandbox.registerComponent('TodoApp', TodoApp);
Sandbox.routes.add([
{ name: 'todoAppRoute', path: 'todo-app', component: Sandbox.components.TodoApp },
]);
...
In the main app
import React from 'react';
import { render } from 'react-dom';
import { Meteor } from 'meteor/meteor';
import { Router, browserHistory } from 'react-router';
import App from './components/App.jsx';
import Homepage from './components/Homepage.jsx';
Sandbox.registerComponent('App', App);
Sandbox.registerComponent('Homepage', Homepage);
Meteor.startup(() => {
const AppRoutes = {
path: '/',
component: Sandbox.components.App,
indexRoute: { name: 'home', component: Sandbox.components.Homepage },
childRoutes: Sandbox.routes.routes,
};
console.log(AppRoutes);
render(
<Router routes={AppRoutes} history={browserHistory} />,
document.getElementById('app-root')
);
});
What is wrong?
I uninstalled all npm packages, meteor packages, updated everything, re-installed latest packages, cleaned out all previous builds and everything works now!
There was something weird somewhere.
If anyone finds themselves in a similar situation, you can try this.
Best