NativeScript handling back button event - nativescript

I am trying to handle the hardware back button in a NativeScript app. I am using NativeScript version 2.3.0 with Angular.
Here is what I have in main.ts file
// this import should be first in order to load some required settings (like globals and reflect-metadata)
import { platformNativeScriptDynamic, NativeScriptModule } from "nativescript-angular/platform";
import { NgModule,Component,enableProdMode } from "#angular/core";
import { AppComponent } from "./app.component";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { routes, navigatableComponents } from "./app.routing";
import { secondComponent } from "./second.component";
import {AndroidApplication} from "application";
#Component({
selector: 'page-navigation-test',
template: `<page-router-outlet></page-router-outlet>`
})
export class PageNavigationApp {
}
#NgModule({
declarations: [AppComponent,PageNavigationApp,secondComponent
// ...navigatableComponents
],
bootstrap: [PageNavigationApp],
providers:[AndroidApplication],
imports: [NativeScriptModule,
NativeScriptRouterModule,
NativeScriptRouterModule.forRoot(routes)
],
})
class AppComponentModule {
constructor(private androidapplication:AndroidApplication){
this.androidapplication.on("activityBackPressed",()=>{
console.log("back pressed");
})
}
}
enableProdMode();
platformNativeScriptDynamic().bootstrapModule(AppComponentModule);
I am importing application with
import {AndroidApplication} from "application";
Then in the constrouctor of appComponentModule I am registering the event for activityBackPressed and just doing a console.log.
This does not work.
What am I missing here?

I'm using NativeScript with Angular as well and this seems to work quite nicely for me:
import { RouterExtensions } from "nativescript-angular";
import * as application from "tns-core-modules/application";
import { AndroidApplication, AndroidActivityBackPressedEventData } from "tns-core-modules/application";
export class HomeComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
if (application.android) {
application.android.on(AndroidApplication.activityBackPressedEvent, (data: AndroidActivityBackPressedEventData) => {
if (this.router.isActive("/articles", false)) {
data.cancel = true; // prevents default back button behavior
this.logout();
}
});
}
}
}
Note that hooking into the backPressedEvent is a global thingy so you'll need to check the page you're on and act accordingly, per the example above.

import { Component, OnInit } from "#angular/core";
import * as Toast from 'nativescript-toast';
import { Router } from "#angular/router";
import * as application from 'application';
#Component({
moduleId: module.id,
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.css']
})
export class MainComponent {
tries: number = 0;
constructor(
private router: Router
) {
if (application.android) {
application.android.on(application.AndroidApplication.activityBackPressedEvent, (args: any) => {
if (this.router.url == '/main') {
args.cancel = (this.tries++ > 0) ? false : true;
if (args.cancel) Toast.makeText("Press again to exit", "long").show();
setTimeout(() => {
this.tries = 0;
}, 2000);
}
});
}
}
}

Normally you should have an android activity and declare the backpress function on that activity. Using AndroidApplication only is not enough. Try this code:
import {topmost} from "ui/frame";
import {AndroidApplication} from "application";
let activity = AndroidApplication.startActivity ||
AndroidApplication.foregroundActivity ||
topmost().android.currentActivity ||
topmost().android.activity;
activity.onBackPressed = function() {
// Your implementation
}
You can also take a look at this snippet for example

As far as I know, NativeScript has a built-in support for this but it's not documented at all.
Using onBackPressed callback, you can handle back button behaviour for View components (e.g. Frame, Page, BottomNavigation).
Example:
function pageLoaded(args) {
var page = args.object;
page.onBackPressed = function () {
console.log("Returning true will block back button default behaviour.");
return true;
};
page.bindingContext = homeViewModel;
}
exports.pageLoaded = pageLoaded;
What's tricky here is to find out which view handles back button press in your app. In my case, I used a TabView that contained pages but the TabView itself handled the event instead of current page.

Related

How to set state or dispatch actions from a navigator (ex : cypress testing)

A good practice given by Cypress (e2e testing) is to set the state of the app programmatically rather than using the UI. This of course makes sense.
On this video https://www.youtube.com/watch?v=5XQOK0v_YRE Brian Mann propose this solution to expose a Redux store :
Is there any possibility with NGXS to have access to the different state programmatically during testing ? An example is for the login process : dispatching directly a Login action or setting the store with the access token, to be logged in before any test, would be nice.
This cofnfiguration works for me:
in app folder in model:
export interface IWindowCypress {
Cypress: {
__store__: Store;
};
}
in app.module.ts:
import {BrowserModule} from '#angular/platform-browser';
import {NgModule} from '#angular/core';
import {NgxsModule, Store} from '#ngxs/store';
import {AppComponent, IWindowCypress} from './app.component';
import {ZooState} from './state/zoo.state';
import {NgxsReduxDevtoolsPluginModule} from '#ngxs/devtools-plugin';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule, NgxsModule.forRoot([ZooState], {}),
NgxsReduxDevtoolsPluginModule.forRoot()
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(protected store: Store) {
const windowSore: IWindowCypress = window as unknown as IWindowCypress;
if (windowSore.Cypress) {
console.log('ustawiƂem store');
windowSore.Cypress.__store__ = store;
}
}
}
using in app component:
import {Component} from '#angular/core';
import {Store} from '#ngxs/store';
import {FeedAnimals} from './state/zoo.state';
/// <reference types="Cypress" />
export interface IWindowCypress {
Cypress: {
__store__: Store;
};
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'cypress-ngxs';
constructor() {
const windowSore: IWindowCypress = window as unknown as IWindowCypress;
if (windowSore.Cypress) {
(windowSore.Cypress.__store__ as Store).dispatch(new FeedAnimals());
}
}
}
using in cypress spec:
/// <reference types="Cypress" />
import {Store} from '#ngxs/store';
import {IWindowCypress} from 'src/app/app.component';
import {FeedAnimals, ZooState} from '../../../src/app/state/zoo.state';
import {Observable} from 'rxjs';
describe('My Second Test Suite', () => {
it('My FirstTest case', () => {
cy.visit(' http://localhost:4200/ ');
cy.get('.content > :nth-child(2)').should(item => {
const windowSore: IWindowCypress = window as unknown as IWindowCypress;
if (windowSore.Cypress) {
// get store
const store: Store = windowSore.Cypress.__store__;
// declare observable
const myObs: Observable<boolean> = store.select(ZooState.zoo$);
// subscribe
myObs.pipe().subscribe((feed) => console.log('from subscribe: ', feed));
// make some dispatch
(windowSore.Cypress.__store__ as Store).dispatch(new FeedAnimals());
(windowSore.Cypress.__store__ as Store).dispatch(new FeedAnimals());
(windowSore.Cypress.__store__ as Store).dispatch(new FeedAnimals());
(windowSore.Cypress.__store__ as Store).dispatch(new FeedAnimals());
}
});
});
});
and zoo state:
import {Injectable} from '#angular/core';
import {Action, Selector, State, StateContext} from '#ngxs/store';
export class FeedAnimals {
static readonly type = '[Zoo] FeedAnimals';
}
export interface ZooStateModel {
feed: boolean;
}
#State<ZooStateModel>({
name: 'zoo',
defaults: {
feed: false
}
})
#Injectable()
export class ZooState {
#Selector()
static zoo$(state: ZooStateModel): boolean {
return state.feed;
}
#Action(FeedAnimals)
feedAnimals(ctx: StateContext<ZooStateModel>): void {
console.log('fedeeeeeed');
const state = ctx.getState();
ctx.setState({
...state,
feed: !state.feed
});
}
}

Nativescript TabView using to much space when adding title text

Is it possible to remove some of the padding inside a tabview item?
The TabView has a lot of empty unused space. If I could remove this then my tabs wouldn't get cutoff. This would greatly improve my app since now you have to swipe a bit to the right to see the full title of the last menu item.
The tabview is created in NS-Vue but I don't think that will matter since this is a native issue.
I'm using negative margins to remove padding from TabView items. That was the only solution I found to do this. This approach is needed only for Android, for iOS it works fine.
import { Component, OnInit, HostListener } from '#angular/core';
import { Page } from 'tns-core-modules/ui/page/page';
import { RouterExtensions } from 'nativescript-angular/router';
import { ActivatedRoute } from '#angular/router';
import { TabView } from 'tns-core-modules/ui/tab-view';
import * as platform from 'tns-core-modules/platform';
#Component({
selector: 'app-tabs',
moduleId: module.id,
templateUrl: './tabs.component.html'
})
export class TabsComponent implements OnInit {
tabView: TabView;
constructor(
private page: Page,
private router: RouterExtensions,
private route: ActivatedRoute
) { }
ngOnInit(): void {
this.tabView = this.page.getViewById('tab');
this.router.navigate([
{
outlets: {
homeTab: ['home'],
organizadorTab: ['organizador'],
favoritosTab: ['favoritos'],
categoriasTab: ['categorias'],
perfilTab: ['perfil']
}
}
], { relativeTo: this.route });
}
#HostListener('loaded')
onLoaded() {
if (platform.isAndroid) {
this.tabView.android.tabLayout.setTabTextFontSize(10);
const viewGroup = this.tabView.android.tabLayout.getChildAt(0);
for (let i = 0; i < viewGroup.getChildCount(); i++) {
const view = viewGroup.getChildAt(i);
const layoutParams = view.getLayoutParams();
layoutParams.width = 1;
layoutParams.leftMargin = -20;
layoutParams.rightMargin = -20;
layoutParams.topMargin = -20;
layoutParams.bottomMargin = -20;
view.setLayoutParams(layoutParams);
}
this.tabView.requestLayout();
}
}
}

common ActionBar and Side Drawer component for all pages native script

I have angular2-nativescript application with several pages. structure is similar to groceries example. All pages has very similar action bar content so I don't want to add all action bar and SideDrawer event handlers for each page or add custom component to each page template
Is there any way to have single ActionBar and SideDrawer component for all application pages? Also it is important to have the ability to access this component from all pages and call its methods from page class (so I can tell this component that it should hide/show some content). I want to use some action bar animation in future so my ActionBar shouldn't be recreated each time page changes
I created components that contain side drawer.
import { Component, ViewChild, OnInit,ElementRef,Input } from '#angular/core';
import { TranslateService } from "ng2-translate";
import {Router, NavigationExtras} from "#angular/router";
import * as ApplicationSettings from "application-settings";
import { Page } from "ui/page";
import { RadSideDrawerComponent, SideDrawerType } from "nativescript-pro-ui/sidedrawer/angular";
import application = require("application");
import { Config } from "../../shared/config";
import * as elementRegistryModule from 'nativescript-angular/element-registry';
import { RouterExtensions } from "nativescript-angular/router";
import { AndroidApplication, AndroidActivityBackPressedEventData } from "application";
import { isAndroid } from "platform";
import { UserService } from "../../shared/user/user.service";
import { SideDrawerLocation } from "nativescript-pro-ui/sidedrawer";
#Component({
selector: 'sideDrawer',
templateUrl: 'shared/sideDrawer/sideDrawer.component.html',
styleUrls: ['shared/sideDrawer/sideDrawer.component.css']
})
export class SideDrawerComponent implements OnInit {
#Input () title =""
#Input () backStatus =true;
theme: string="shared/sideDrawer/sideDrawer.component.ar.css";
private drawer: SideDrawerType;
private isEnglish=true;
#ViewChild(RadSideDrawerComponent)
public drawerComponent: RadSideDrawerComponent;
constructor(private us: UserService,private translate: TranslateService,private router:Router, private routerExtensions: RouterExtensions,private _page: Page) { }
ngOnInit() {
this.drawer = this.drawerComponent.sideDrawer;
this.drawer.showOverNavigation=true;
if (ApplicationSettings.getString("language")=="ar") {
this.isEnglish=false;
this.drawer.drawerLocation = SideDrawerLocation.Right;
this.addArabicStyleUrl();
}
if (!isAndroid) {
return;
}
application.android.on(AndroidApplication.activityBackPressedEvent, (data: AndroidActivityBackPressedEventData) => {
data.cancel = true; // prevents default back button behavior
this.back() ;
});
}
back() {
this.routerExtensions.back();
}
public toggleShow(){
this.drawer.showDrawer();
}
add it in every page and customize it using input and output parameters
<sideDrawer [title]="'category' | translate"></sideDrawer>

react-navigation - Cannot read property 'navigate' of undefined

I'm new in React Native and trying create my first app. So I have a question:
I got 2 screens (using react-navigation). At first screen there is a render of app logo with spinner(from native-base) and fetch to the server at the same time. And I need to navigate to another screen only when fetch is over and responce is handled. Please help me find my mistakes!
index.ios.js
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
TextInput,TouchableHighlight
} from 'react-native';
import { StackNavigator } from 'react-navigation';
import LoadingScreen from './src/screens/LoadingScreen.js';
import MainContainer from './src/screens/MainContainer.js';
export default class Calculator2 extends Component {
render() {
return (
<LoadingScreen/>
);
}
}
const AppNavigator = StackNavigator({
Loading: {
screen: LoadingScreen
},
Main: {
screen: MainContainer
}
});
AppRegistry.registerComponent('Calculator2', () => Calculator2);
LoadingScreen.js:
import React, { Component } from 'react';
import {
AsyncStorage,
AppRegistry,NetInfo,
Text,Image,View
} from 'react-native';
import { StackNavigator } from 'react-navigation';
import AppNavigator from '../../index.ios.js';
import { Container, Header, Content, Spinner } from 'native-base';
export default class LoadingScreen extends Component {
static navigationOptions = {
title: 'Loading',
};
constructor(props){
super(props);
}
componentDidMount(){
const {navigate} = this.props.navigation;
fetch('url').then( (response) => {navigate('Main')});
}
render() {
return(
<View>
App logo with spinner
</View>
);
}
}
MainContainer.js
import React, { Component } from 'react';
import {
AppRegistry,Alert,NetInfo,
StyleSheet,
Text,
View,ActivityIndicator,
TextInput,TouchableHighlight
} from 'react-native';
import { StackNavigator } from 'react-navigation';
import AppNavigator from '../../index.ios.js';
export default class MainContainer extends Component {
static navigationOptions = {
title: 'Main',
};
render() {
return (
<View style={{flexDirection: 'column'}}>
...
</View>
);
}
}
And all I got is an error "Cannot read property 'navigate' of undefined" at LoadingScreen.componentDidMount
UPD
actually my fetch should be a function getting responce and handling it, and it should wait till handling is done:
async function getData(){
var response = await fetch('url', {
method: 'GET'
});
storage = await response.json(); // storage for response
regions = Object.keys(storage); // an array of regions names
console.log(storage, Object.keys(storage));
};
You need to register AppNavigator component instead of Calculator2
AppRegistry.registerComponent('Calculator2', () => AppNavigator);
Just update your LoadingScreen.js's componentDidMount function as following:
componentDidMount() {
var self = this;
fetch('url').then( (response) => {
self.props.navigation.navigate('Main')
});
}

componentDidUpdate does not fire

So I've been struggling to figure out the react-redux ecosystem for a while now. I'm almost there but there is still something that keep giving is me issues, and that's the componentDidUpdate method. When I dispatch an async action, the store is reducer is called correctly and the component's state does update.
But for some reason, the componentDidUpdate method does not fire, there is no re-render, and I cannot access the updated props. I can see it change in devtools, if I console.log(this.props.blogStore). At first it shows as an empty object but when on click it opens and shows the updated state.
I've tried as many life cycle methods as I can but nothing seems to work, including componentWillReceiveProps.
Any idea what I'm doing wrong?
Here is the code:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import Datastore from 'Datastore';
const store = Datastore()
store.subscribe(() => console.log("state changed", store.getState()))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);
Datastore.js
import { combineReducers, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import Mainmenu from 'reducers/Mainmenu';
import Blogstore from 'reducers/Blogstore';
const reducer = combineReducers({
Mainmenu,
Blogstore,
})
export default function Datastore() {
const store = createStore(
reducer,
applyMiddleware(thunk)
)
return store
}
reducer
import Article from 'lib/Article';
import { ARTICLE_LOAD, ARTICLE_UPDATE, SAVE_ARTICLE_LIST } from 'actionTypes';
const initialBlogState = {
}
const Blogstore = (state=initialBlogState, action) => {
switch(action.type) {
case SAVE_ARTICLE_LIST:
state.init = true
state.articles = action.payload
return state
case ARTICLE_LOAD:
return state
case ARTICLE_UPDATE:
return state
}
return state
}
export default Blogstore;
blog-actions.js
import { ARTICLE_LOAD, ARTICLE_UPDATE, SAVE_ARTICLE_LIST } from 'actionTypes';
import APIFetch from '../lib/Fetch';
export function getArticlePids() {
return dispatch => {
APIFetch().get("/blog/list").then(response => {
dispatch({
type: SAVE_ARTICLE_LIST,
payload: response.data
})
})
}
}
component
import React from 'react';
import { connect } from 'react-redux';
import * as blogActions from '../actions/blog-actions';
#connect(state => ({
blogStore: state.Blogstore
}))
export default class Blog extends React.Component {
constructor() {
super()
}
componentDidMount() {
this.props.dispatch(blogActions.getArticlePids())
}
componentDidUpdate(prevProps) {
console.log("update", prevProps)
}
render() {
console.log("render", this.props.blogStore)
return (
<div><h1>Blog</h1></div>
)
}
}
That is pretty much it. I won't bother pasting the App and Router that are between index.js and the component because there is nothing of interest there. Just a basic react router and components that have nothing to do with this.
You need to return a new object from your reducer, like this:
import Article from 'lib/Article';
import { ARTICLE_LOAD, ARTICLE_UPDATE, SAVE_ARTICLE_LIST } from 'actionTypes';
const initialBlogState = {
}
const Blogstore = (state=initialBlogState, action) => {
switch(action.type) {
case SAVE_ARTICLE_LIST:
return Object.assign({}, state, {
init: true,
articles: action.payload,
})
case ARTICLE_LOAD:
return state
case ARTICLE_UPDATE:
return state
}
return state
}
export default Blogstore;
Otherwise, if you try to update your state directly (as you are doing currently) it will only mutate the internal reference of the state and react components won't be able to detect the change and wont re-render. Read more here.

Resources