Related
I am using ionic with capacitor and angularfire. It works fine but does not work on ios. On ios I don't see any error - just the firebase call that never returns anything. Below is my package.json and other relevant info.
ionic.info:
ionic info
Ionic:
Ionic CLI : 6.18.1 (/usr/local/lib/node_modules/#ionic/cli)
Ionic Framework : #ionic/angular 5.9.1
#angular-devkit/build-angular : 12.1.4
#angular-devkit/schematics : 12.1.4
#angular/cli : 12.1.4
#ionic/angular-toolkit : 4.0.0
Capacitor:
Capacitor CLI : 3.3.2
#capacitor/android : 3.3.2
#capacitor/core : 3.3.2
#capacitor/ios : 3.3.2
Cordova:
Cordova CLI : 10.0.0 (cordova-lib#10.1.0)
Cordova Platforms : android broken, ios 5.1.1
Cordova Plugins : no whitelisted plugins (0 plugins total)
Utility:
cordova-res (update available: 0.15.4) : 0.15.3
native-run : 1.5.0
System:
ios-deploy : 1.9.4
ios-sim : ios-sim/9.0.0 darwin-x64 node-v14.17.0
NodeJS : v14.17.0 (/usr/local/bin/node)
npm : 7.24.0
OS : macOS Monterey
Xcode : Xcode 13.1 Build version 13A1030d
Package.json:
"dependencies": {
"#angular/common": "~12.1.1",
"#angular/core": "~12.1.1",
"#angular/fire": "^7.2.0",
"#angular/forms": "~12.1.1",
"#angular/platform-browser": "~12.1.1",
"#angular/platform-browser-dynamic": "~12.1.1",
"#angular/router": "~12.1.1",
"#capacitor/android": "^3.3.2",
"#capacitor/app": "^1.0.6",
"#capacitor/browser": "^1.0.6",
"#capacitor/core": "3.3.2",
"#capacitor/device": "^1.1.0",
"#capacitor/haptics": "^1.1.3",
"#capacitor/ios": "^3.3.2",
"#capacitor/keyboard": "^1.1.3",
"#capacitor/splash-screen": "^1.1.6",
"#capacitor/status-bar": "^1.0.6",
"#capacitor/storage": "^1.2.3",
"#ionic-native/app-version": "^5.36.0",
"#ionic-native/barcode-scanner": "^5.36.0",
"#ionic-native/call-number": "^5.36.0",
"#ionic-native/camera": "^5.36.0",
"#ionic-native/camera-preview": "^5.36.0",
"#ionic-native/contacts": "^5.36.0",
"#ionic-native/email-composer": "^5.36.0",
"#ionic-native/in-app-browser": "^5.36.0",
"#ionic-native/in-app-purchase": "^5.36.0",
"#ionic-native/in-app-purchase-2": "^5.36.0",
"#ionic-native/social-sharing": "^5.36.0",
"#ionic/angular": "^5.5.2",
"#ngx-translate/core": "^11.0.1",
"#ngx-translate/http-loader": "^4.0.0",
"call-number": "^1.0.1",
"cordova-plugin-app-version": "^0.1.12",
"cordova-plugin-camera": "^6.0.0",
"cordova-plugin-camera-preview": "^0.12.3",
"cordova-plugin-contacts": "^3.0.1",
"cordova-plugin-email-composer": "^0.10.0",
"cordova-plugin-inappbrowser": "^5.0.0",
"cordova-plugin-inapppurchase": "^1.2.0",
"cordova-plugin-purchase": "^10.6.1",
"cordova-plugin-x-socialsharing": "^6.0.3",
"firebase": "^9.5.0",
"forcejs": "^2.2.1",
"phonegap-plugin-barcodescanner": "^8.1.0",
"rxjs": "~6.6.0",
"tslib": "^2.2.0",
"xcode": "^3.0.1",
"xml-js": "^1.6.11",
"zone.js": "~0.11.4"
},
"devDependencies": {
"#angular-devkit/build-angular": "~12.1.1",
"#angular-eslint/builder": "~12.0.0",
"#angular-eslint/eslint-plugin": "~12.0.0",
"#angular-eslint/eslint-plugin-template": "~12.0.0",
"#angular-eslint/template-parser": "~12.0.0",
"#angular/cli": "~12.1.1",
"#angular/compiler": "~12.1.1",
"#angular/compiler-cli": "~12.1.1",
"#angular/language-service": "~12.0.1",
"#capacitor/cli": "3.3.2",
"#ionic/angular-toolkit": "^4.0.0",
"#types/jasmine": "~3.6.0",
"#types/jasminewd2": "~2.0.3",
"#types/node": "^12.11.1",
"#typescript-eslint/eslint-plugin": "4.16.1",
"#typescript-eslint/parser": "4.16.1",
"es6-promise-plugin": "^4.2.2",
"eslint": "^7.6.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsdoc": "30.7.6",
"eslint-plugin-prefer-arrow": "1.2.2",
"jasmine-core": "~3.8.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"typescript": "~4.2.4"
}
app.module.ts:
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { RouteReuseStrategy } from '#angular/router';
import { AngularFireModule } from '#angular/fire/compat';
import { AngularFireDatabaseModule } from '#angular/fire/compat/database';
import { IonicModule, IonicRouteStrategy } from '#ionic/angular';
import { FormsModule } from '#angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
#NgModule({
declarations: [
AppComponent,
],
entryComponents: [
],
imports: [
AngularFireModule.initializeApp(firebaseConfig),
AngularFireDatabaseModule,
BrowserModule,
HttpClientModule,
IonicModule.forRoot(),
AppRoutingModule,
FormsModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}
The code that never executes and no error in catch block is below. Same works just fine on android:
const resp:any = await this.dataSvc.getSeedConfig()
console.log("seed config is::" + JSON.stringify(resp))
So the above code does not go to the catch block and does not print the seed config on ios. Works fine on android.
Also on ios it works using live reload using
ionic capacitor run ios -l --external
so if i run it using xcode then it does not work. not sure what is blocking in that specific case
Already answered this here https://stackoverflow.com/a/70594992/5701521 but could be useful to answer it here as well.
Been struggling a lot with this issue too but I managed to fix it. For those who need help here's my code.
You can delete all Firebase related imports from app.module.ts since this solution only uses Firebase.
The packages rxfire and #angular/fire can be removed from your package.json. The only dependency I have is "firebase": "^9.6.1".
I used observables for the getObject and list functions since that's what I'm used to and I didn't want to rewrite my original code.
import { Injectable } from '#angular/core';
import { Capacitor } from '#capacitor/core';
import { environment } from '#environment';
import { initializeApp } from 'firebase/app';
import { Auth, getAuth, indexedDBLocalPersistence, initializeAuth, signInWithCustomToken } from 'firebase/auth';
import { Database, getDatabase, onValue, orderByChild, query, ref } from 'firebase/database';
import { Observable, Observer, from } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class FirebaseService {
private readonly database: Database;
private readonly auth: Auth;
constructor() {
const firebaseApp = initializeApp(environment.firebase);
if (Capacitor.isNativePlatform()) {
initializeAuth(firebaseApp, {
persistence: indexedDBLocalPersistence
});
}
this.database = getDatabase(firebaseApp);
this.auth = getAuth(firebaseApp);
}
connectFirebase(firebaseToken) {
return from(signInWithCustomToken(this.auth, firebaseToken));
}
disconnectFirebase() {
return from(this.auth.signOut());
}
getObject<T>(path: string): Observable<T> {
return new Observable((observer: Observer<T>) => {
const dbRef = ref(this.database, path);
const listener = onValue(dbRef, snapshot => {
const data = snapshot.val();
observer.next(data);
});
return {
unsubscribe() {
listener();
}
};
});
}
public list<T>(path: string, orderChildBy?: string): Observable<Array<T>> {
return new Observable<Array<T>>((observer: Observer<Array<T>>) => {
const dbRef = ref(this.database, path);
const dbReference = !orderChildBy ? dbRef : query(dbRef, orderByChild(orderChildBy));
const listener = onValue(dbReference, snapshot => {
const data = Object.values<T>(snapshot.val() || {});
console.log(path, data);
observer.next(data);
});
return {
unsubscribe() {
listener();
}
};
});
}
}
For those who can't see the error message thrown by firebase try the following command in your Safari console to see the error.
window.location.reload()
Try to downgrade your angularfire to v7.0 it might work with firebase v9.
in my case i folow this link: https://github.com/angular/angularfire/issues/3087#issuecomment-990599459
I was also having this issue when building for an iOS emulator and actual iOS device, but adding this to the app.component.ts worked for me.
import { Component } from '#angular/core';
import { Capacitor } from '#capacitor/core';
import { initializeApp } from 'firebase/app';
import { indexedDBLocalPersistence, initializeAuth } from 'firebase/auth';
import { environment } from 'src/environments/environment';
#Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor() {
const app = initializeApp(environment.firebase);
if (Capacitor.isNativePlatform) {
initializeAuth(app, {
persistence: indexedDBLocalPersistence
});
}
}
}
This is where I found the fix and a sample code
My code is in Angular7, using #ngrx/store7, and sometimes I get undefined state (just for a specific state)
It is always the "history" state.
My App State:
export interface IAppState {
router?: RouterReducerState,
delivery: IDeliveryState,
events: IEventsState,
families: IFamiliesState,
googleDrive: IGoogleDriveState,
spinner: ISpinnerState,
user: IUserState,
volunteerConstraints: IVolunteerConstraintsState,
volunteers: IVolunteersState,
history: IHistoryState
}
export const initialAppState: IAppState = {
delivery: initialDeliveryState,
events: initialEventsState,
families: initialFamiliesState,
googleDrive: initialGoogleDriveState,
spinner: initialSpinnerState,
user: initialUserState,
volunteerConstraints: initialVolunteerConstraintsState,
volunteers: initialVolunteersState,
history: initialHistoryState
};
export function getInitialState(): IAppState {
return initialAppState;
}
My history state:
export interface IHistoryState {
familyHistory: Array<FamilyHistoryModel>;
familyHistoryPending: boolean;
routeHistory: Array<RouteHistoryModel>;
routeHistoryPending: boolean;
deliveryHistory: Array<DeliveryHistoryChartModel>;
volunteerRating: Array<VolunteerRatingModel>;
volunteerRatingPending: number;
initiated: boolean;
}
export const initialHistoryState: IHistoryState = {
familyHistory: new Array<FamilyHistoryModel>(),
familyHistoryPending: false,
routeHistory: new Array<RouteHistoryModel>(),
routeHistoryPending: false,
deliveryHistory: new Array<DeliveryHistoryChartModel>(),
volunteerRating: new Array<VolunteerRatingModel>(),
volunteerRatingPending: 0,
initiated: true
};
My History selector:
const selectHistoryState = (state: IAppState) => state.history;
export const selectFamilyHistory = createSelector(
selectHistoryState,
(state: IHistoryState) => state.familyHistory
);
export const selectFamilyHistoryPending = createSelector(
selectHistoryState,
(state: IHistoryState) => state.familyHistoryPending
);
export const selectRouteHistory = createSelector(
selectHistoryState,
(state: IHistoryState) => state.routeHistory
);
export const selectRouteHistoryPending = createSelector(
selectHistoryState,
(state: IHistoryState) => state.routeHistoryPending
);
export const selectDeliveryHistory = createSelector(
selectHistoryState,
(state: IHistoryState) => state.deliveryHistory
);
export const selectVolunteerRating = createSelector(
selectHistoryState,
(state: IHistoryState) => state.volunteerRating
);
export const selectVolunteerRatingPending = createSelector(
selectHistoryState,
(state: IHistoryState) => state.volunteerRatingPending
);
My app reducer:
export const appReducers: ActionReducerMap<IAppState, any> = {
router: routerReducer,
delivery: deliveryReducer,
events: eventsReducer,
families: familiesReducer,
googleDrive: googleDriveReducer,
spinner: spinnerReducer,
user: userReducer,
volunteerConstraints: volunteerConstraintsReducer,
volunteers: volunteersReducer,
history: historyReducer
}
My History reducer:
export function historyReducer(
state = initialHistoryState,
action: actions.HistoryActions): IHistoryState {
switch (action.type) {
case actions.HistoryActionTypes.Initiate:
return {
...state,
initiated: true
};
case actions.HistoryActionTypes.GetFamilyHistory:
return {
...state,
familyHistory: initialHistoryState.familyHistory,
familyHistoryPending:true
};
case actions.HistoryActionTypes.GetFamilyHistorySuccess:
return {
...state,
familyHistory: action.payload,
familyHistoryPending: false
};
case actions.HistoryActionTypes.GetFamilyHistoryFailure:
return {
...state,
familyHistoryPending: false
};
case actions.HistoryActionTypes.GetRouteHistory:
return {
...state,
routeHistory: initialHistoryState.routeHistory,
routeHistoryPending: true
};
case actions.HistoryActionTypes.GetRouteHistorySuccess:
return {
...state,
routeHistory: action.payload,
routeHistoryPending: false
};
case actions.HistoryActionTypes.GetRouteHistoryFailure:
return {
...state,
routeHistoryPending: false
};
case actions.HistoryActionTypes.GetDeliveryHistory:
return {
...state,
deliveryHistory: initialHistoryState.deliveryHistory
};
case actions.HistoryActionTypes.GetDeliveryHistorySuccess:
return {
...state,
deliveryHistory: action.payload
};
case actions.HistoryActionTypes.GetVolunteersRating:
return {
...state,
volunteerRating: initialHistoryState.volunteerRating,
volunteerRatingPending: state.volunteerRatingPending + 1
};
case actions.HistoryActionTypes.GetVolunteersRatingSuccess:
return {
...state,
volunteerRating: action.payload,
volunteerRatingPending: state.volunteerRatingPending - 1
};
case actions.HistoryActionTypes.GetVolunteersRatingFailure:
return {
...state,
volunteerRatingPending: state.volunteerRatingPending - 1 < 0 ? 0 :
state.volunteerRatingPending - 1
};
}
}
At the beginning the history state is undefined (as shown in the picture), and only after I dispatch an history action and change the state in the reducer, it gets initiated.
How do I fix that to be initiated at the beginning as all other states?
packge.json:
"dependencies": {
"#agm/core": "^1.0.0-beta.3",
"#agm/js-marker-clusterer": "^1.0.0-beta.3",
"#angular-mdl/core": "^6.0.1",
"#angular-mdl/datepicker": "^6.0.0",
"#angular-mdl/expansion-panel": "^6.0.0",
"#angular-mdl/popover": "^6.0.0",
"#angular-mdl/select": "^6.0.0",
"#angular/animations": "~7.1.0",
"#angular/cdk": "^7.2.2",
"#angular/common": "~7.1.0",
"#angular/compiler": "~7.1.0",
"#angular/core": "~7.1.0",
"#angular/forms": "~7.1.0",
"#angular/http": "^7.2.2",
"#angular/material": "^7.2.2",
"#angular/platform-browser": "~7.1.0",
"#angular/platform-browser-dynamic": "~7.1.0",
"#angular/router": "~7.1.0",
"#ngrx/effects": "^7.1.0",
"#ngrx/router-store": "^7.1.0",
"#ngrx/schematics": "^7.1.0",
"#ngrx/store": "^7.1.0",
"#ngrx/store-devtools": "^7.1.0",
"#ngx-translate/core": "^10.0.2",
"#ngx-translate/http-loader": "^3.0.1",
"#swimlane/ngx-charts": "^10.0.0",
"angular-2-local-storage": "^2.0.0",
"angular2-moment": "^1.9.0",
"angular2localization": "^1.4.2",
"aos": "^2.3.4",
"core-js": "^2.5.4",
"crypto-js": "^3.1.9-1",
"hammer-timejs": "^1.1.0",
"hammerjs": "^2.0.8",
"js-marker-clusterer": "^1.0.0",
"jssha": "^2.3.1",
"lodash": "^4.17.4",
"lodash-es": "^4.17.4",
"moment": "^2.20.1",
"ngrx-store-freeze": "^0.1.9",
"ngrx-store-localstorage": "^0.1.8",
"ngrx-store-logger": "^0.1.8",
"ngx-chips": "^1.4.5",
"ngx-device-detector": "^1.3.0",
"ngx-gallery": "^5.6.2",
"ngx-infinite-scroll": "^0.5.1",
"rxjs": "~6.3.3",
"rxjs-compat": "^6.0.0",
"sha.js": "^2.4.11",
"tslib": "^1.9.0",
"videogular2": "^6.3.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"#angular-devkit/build-angular": "~0.11.0",
"#angular/cli": "~7.1.4",
"#angular/compiler-cli": "~7.1.0",
"#angular/language-service": "~7.1.0",
"#types/core-js": "^2.5.0",
"#types/googlemaps": "^3.30.16",
"#types/jasmine": "~2.8.8",
"#types/jasminewd2": "~2.0.3",
"#types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.6"
}
Looks to me like you are missing default case in your history reducer.
switch(...) {
case: ...
case: ...
default: {
return state;
}
}
I expect that the Effect that I created should be triggered by dispatching an action, but it is not happening.
The action is being dispatched correctly, but the effect seems that is not properly watching this.
I created this effect:
import { Injectable } from '#angular/core';
import { Actions, Effect, ofType } from '#ngrx/effects';
import { Action } from '#ngrx/store';
import { Observable, of } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { PoliciesService } from 'app/services/policies';
import * as policiesActions from '../actions/policies.actions';
import { Policy } from 'app/dtos/policy.dto';
import { PoliciesMapper } from 'app/services/policies/policies.mapper';
Injectable();
export class PoliciesEffect {
constructor(
private actions$: Actions,
private policyService: PoliciesService
) { }
#Effect()
getPolicies$: Observable<Action> = this.actions$.pipe(
ofType<policiesActions.GetPoliciesAction>(
policiesActions.ActionPoliciesType.GET_POLICIES
),
mergeMap((actions: policiesActions.GetPoliciesAction) =>
this.policyService.getPolicies().pipe(
map((policies: Policy[]) =>
new policiesActions.GetPoliciesActionSuccess(PoliciesMapper.policiesCollectionToPoliciesModelCollection(policies))
),
catchError(err => of(new policiesActions.GetPoliciesActionFailed(err)))
)
)
);
}
This effect expect to be triggered once the Action GetPoliciesAction is being dispatch.
This is not happening so I am not able to do the HTTP request to get my data from the server.
Here are the Actions that I created using #ngrx/store:
import { Action } from '#ngrx/store';
import { Policy } from 'app/dtos/policy.dto';
import { PolicyModel } from 'app/models/policy.model';
export enum ActionPoliciesType {
GET_POLICIES = '[Policies] Get all policies list',
GET_POLICIES_SUCCESS = '[Policies] Policies received succesfully',
GET_POLICIES_FAILED = '[Policies] Policies reception failed'
}
export class GetPoliciesAction implements Action {
readonly type = ActionPoliciesType.GET_POLICIES;
constructor() { }
}
export class GetPoliciesActionSuccess implements Action {
readonly type = ActionPoliciesType.GET_POLICIES_SUCCESS;
constructor(public payload: PolicyModel[]) { }
}
export class GetPoliciesActionFailed implements Action {
readonly type = ActionPoliciesType.GET_POLICIES_FAILED;
constructor(public payload: string) { }
}
export type PoliciesActions =
GetPoliciesAction |
GetPoliciesActionSuccess |
GetPoliciesActionFailed;
And here is my app.module.ts:
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { MaterialModule } from './material';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NavbarComponent } from './components/navbar/navbar.component';
import { FooterComponent } from './components/footer/footer.component';
import { HomeComponent } from './components/home/home.component';
import { AngularFireModule } from 'angularfire2';
import { AngularFireDatabaseModule } from 'angularfire2/database';
import { StoreModule } from '#ngrx/store';
import { environment } from '../environments/environment';
import { IPoliciesService } from './services/policies/policies.interface';
import { PoliciesService } from './services/policies';
import { reducers } from './redux/reducers/reducers';
import { StoreDevtoolsModule } from '#ngrx/store-devtools';
import { EffectsModule, Actions} from '#ngrx/effects';
import { HttpClientModule} from '#angular/common/http';
#NgModule({
declarations: [
AppComponent,
NavbarComponent,
FooterComponent,
HomeComponent
],
imports: [
BrowserModule,
AppRoutingModule,
MaterialModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFireDatabaseModule,
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument(),
EffectsModule.forRoot([]),
HttpClientModule
],
providers: [{
provide: IPoliciesService,
useClass: PoliciesService
}],
bootstrap: [AppComponent]
})
export class AppModule { }
package.json:
"dependencies": {
"#angular/animations": "^7.0.4",
"#angular/cdk": "^7.3.0",
"#angular/common": "~7.0.0",
"#angular/compiler": "~7.0.0",
"#angular/core": "~7.0.0",
"#angular/forms": "~7.0.0",
"#angular/http": "~7.0.0",
"#angular/material": "^7.3.0",
"#angular/platform-browser": "~7.0.0",
"#angular/platform-browser-dynamic": "~7.0.0",
"#angular/router": "~7.0.0",
"#ngrx/effects": "^7.2.0",
"#ngrx/store": "^7.2.0",
"#ngrx/store-devtools": "^7.2.0",
"angularfire2": "^5.1.1",
"concurrently": "^4.1.0",
"core-js": "^2.5.4",
"firebase": "^5.8.1",
"hammerjs": "^2.0.8",
"rxjs": "~6.3.3",
"zone.js": "~0.8.26"
},
"devDependencies": {
"#angular-devkit/build-angular": "~0.10.0",
"#angular/cli": "~7.0.3",
"#angular/compiler-cli": "~7.0.0",
"#angular/language-service": "~7.0.0",
"#types/node": "~8.9.4",
"#types/jasmine": "~2.8.8",
"#types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
}
You are not providing your effects class to the effects module. In your ngModule import for EffectsModule you are passing an empty array. Your PoliciesEffects service should be in that array.
I have an ionic 3 app.
On a page I have a form with some fields.
<form>
<ion-item>
<ion-label>First item</ion-label>
<ion-input type="text" [(ngModel)]="title" name="title"></ion-input>
</ion-item>
... some more simple fields ...
<ion-item>
<ion-label>Item below keyboard region</ion-label>
<ion-textarea [(ngModel)]="description" name="description"></ion-textarea>
</ion-item>
<button ion-button type="submit" block>Add Todo</button>
</form>
When I tab the first, the keyboard is shown and the input item is properly focussed, that is: shows a blinking caret.
Though, when I click a field at a position below the area needed to show the keyboard, I do not get a caret, although the field is actually focussed. When I type, the karakters are put in the field.
The main difference is that when clicking on the lower field, the form is shifted upwards when the keyboard shows.
How to fix this?
I am running the app on an iPad 2017, iOS 11.2.2.
package.json:
{
"name": "my app",
"version": "1.0.1",
"author": "Ionic Framework",
"homepage": "http://ionicframework.com/",
"private": true,
"scripts": {
"clean": "ionic-app-scripts clean",
"build": "ionic-app-scripts build --release",
"lint": "ionic-app-scripts lint",
"ionic:build": "ionic-app-scripts build",
"ionic:serve": "ionic-app-scripts serve"
},
"dependencies": {
"#angular/animations": "5.0.0",
"#angular/common": "5.0.0",
"#angular/compiler": "5.0.0",
"#angular/compiler-cli": "5.0.0",
"#angular/core": "5.0.0",
"#angular/forms": "5.0.0",
"#angular/http": "5.0.0",
"#angular/platform-browser": "5.0.0",
"#angular/platform-browser-dynamic": "5.0.0",
"#ionic-native/app-version": "^4.5.2",
"#ionic-native/calendar": "^4.3.2",
"#ionic-native/call-number": "^4.4.2",
"#ionic-native/camera": "^4.3.2",
"#ionic-native/core": "4.3.0",
"#ionic-native/date-picker": "^4.4.2",
"#ionic-native/file": "^4.4.2",
"#ionic-native/in-app-browser": "^4.3.3",
"#ionic-native/keyboard": "^4.4.2",
"#ionic-native/media-capture": "^4.4.0",
"#ionic-native/native-page-transitions": "^4.3.2",
"#ionic-native/splash-screen": "4.3.0",
"#ionic-native/status-bar": "4.3.0",
"#ionic/pro": "^1.0.9",
"#ionic/storage": "2.0.1",
"#ngx-translate/core": "^9.1.0",
"#ngx-translate/http-loader": "^2.0.1",
"call-number": "^1.0.1",
"com.telerik.plugins.nativepagetransitions": "^0.6.5",
"cordova-ios": "^4.5.4",
"cordova-plugin-app-version": "^0.1.9",
"cordova-plugin-calendar": "^4.6.0",
"cordova-plugin-camera": "^2.4.1",
"cordova-plugin-compat": "^1.2.0",
"cordova-plugin-datepicker": "^0.9.3",
"cordova-plugin-device": "^1.1.7",
"cordova-plugin-file": "^5.0.0",
"cordova-plugin-file-transfer": "^1.7.0",
"cordova-plugin-inappbrowser": "^1.7.2",
"cordova-plugin-ionic-webview": "^1.1.16",
"cordova-plugin-media-capture": "^1.4.3",
"cordova-plugin-privacyscreen": "^0.4.0",
"cordova-plugin-splashscreen": "^4.1.0",
"cordova-plugin-statusbar": "^2.3.0",
"cordova-plugin-whitelist": "^1.3.3",
"cordova-windows": "^5.0.0",
"intl": "^1.2.5",
"ionic-angular": "3.9.2",
"ionic-plugin-keyboard": "^2.2.1",
"ionicons": "3.0.0",
"mx.ferreyra.callnumber": "0.0.2",
"ng2-datepicker": "^2.2.1",
"plist": "^2.1.0",
"rxjs": "5.5.2",
"sw-toolbox": "3.6.0",
"zone.js": "0.8.18"
},
"devDependencies": {
"#ionic/app-scripts": "3.1.0",
"cors": "^2.8.4",
"typescript": "2.4.2",
"ws": "3.3.2"
},
"description": "An Ionic project",
"cordova": {
"plugins": {
"com.telerik.plugins.nativepagetransitions": {},
"cordova-plugin-camera": {
"CAMERA_USAGE_DESCRIPTION": " ",
"PHOTOLIBRARY_USAGE_DESCRIPTION": " "
},
"cordova-plugin-calendar": {
"CALENDAR_USAGE_DESCRIPTION": "This app uses your calendar to plan sessions."
},
"cordova-plugin-privacyscreen": {},
"ionic-plugin-keyboard": {},
"cordova-plugin-whitelist": {},
"cordova-plugin-device": {},
"cordova-plugin-splashscreen": {},
"cordova-plugin-ionic-webview": {},
"cordova-plugin-inappbrowser": {},
"cordova-plugin-media-capture": {
"CAMERA_USAGE_DESCRIPTION": " ",
"MICROPHONE_USAGE_DESCRIPTION": " ",
"PHOTOLIBRARY_USAGE_DESCRIPTION": " "
},
"cordova-plugin-datepicker": {},
"mx.ferreyra.callnumber": {},
"cordova-plugin-statusbar": {},
"call-number": {},
"cordova-plugin-file": {
"PHOTOLIBRARY_USAGE_DESCRIPTION": "This allows",
"PHOTOLIBRARY_ADD_USAGE_DESCRIPTION": "This allows",
"FILE_USAGE_DESCRIPTION": "This app uses your files to upload on sessions.",
"CAMERA_USAGE_DESCRIPTION": " ",
"MICROPHONE_USAGE_DESCRIPTION": " "
},
"cordova-plugin-app-version": {}
},
"platforms": [
"windows",
"ios"
]
}
}
app.module.ts:
...
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
IonicModule.forRoot(MyApp, {scrollAssist: false, autoFocusAssist: 'delay'})
],
in app.component.ts:
this.platform.ready().then(() => {
console.log('Platform is ready!');
this.keyboard.disableScroll(false);
...
thanks!
I have this hackish solution, but I guess there have to be a better one. But because of the comment of user2158259, I'll post it anyway. Please don't punish me for posting it ;-)
1) Remove packages/plugins if you have them:
ionic cordova plugin remove ionic-plugin-keyboard
npm uninstall ionic-plugin-keyboard --save
npm uninstall #ionic-native/keyboard --save
2) Remove any references in app.module.ts such as
this.keyboard.disableScroll(true);
3) create a sub-folder in the app-folder and add these two files:
device.service.ts
The following code is a subset of my device.service.ts
import {ApplicationRef, EventEmitter, Injectable} from '#angular/core';
#Injectable()
export class DeviceService {
public onTick: EventEmitter<string> = new EventEmitter();
private tickValue = new Date().getTime();
constructor(public appRef: ApplicationRef) {
window.addEventListener('onresize', () => {
console.log('DeviceService: on resize');
this.doTick();
setTimeout(() => {
this.doTick();
}, 100);
}, false);
window.addEventListener('transitionend', () => {
console.log('transition ended');
this.doTick();
}, false);
this.tickValue = new Date().getTime();
setTimeout(() => {
this.doTick();
});
}
/**
* getTickValue() returns a different value when something changed (orientation, keyboard up/down).
* by adding this to the screen, the Ionic caret will be adjusted properly
*/
getTickValue(): string {
return this.tickValue + ' ' + window.innerWidth + ' ' + window.innerHeight + ' ' + window.orientation;
}
doTick(): void {
this.tickValue = new Date().getTime();
this.onTick.emit(String(this.tickValue));
this.appRef.tick();
}
}
kb-scroll.ts
import {ApplicationRef, Directive, ElementRef, HostListener, Renderer2} from '#angular/core';
import {DeviceService} from './device.service';
#Directive({
selector: '[kb-scroll]' // Attribute selector
})
export class KbScrollDirective {
constructor(public appRef: ApplicationRef,
public elm: ElementRef,
public renderer: Renderer2,
public device: DeviceService) {
}
#HostListener('click', ['$event'])
onClick($event) {
let elmClickedY;
let scrollContent: HTMLElement;
if ('TEXTAREA' === this.elm.nativeElement.tagName) {
scrollContent = $event.toElement;
elmClickedY = $event.offsetY;
} else if ('ION-CONTENT' === this.elm.nativeElement.tagName) {
// calculate Y offset between click and top of scroll-content area
scrollContent = this.elm.nativeElement.querySelector('.scroll-content');
if (scrollContent) {
// $event.offsetY is most likely small offset in clicked input field in scroll content
// calculate the offsetY opposed to the container (div.scroll-content)
let clickScreenY = $event.toElement.getBoundingClientRect().top + $event.offsetY;
let scrollContentScreenY = scrollContent.getBoundingClientRect().top;
elmClickedY = clickScreenY - scrollContentScreenY;
} else {
console.warn('KbScrollDirective: could not find .scroll-content div in ', this.elm.nativeElement);
}
} else {
console.warn('KbScrollDirective: Can\'t handle ', this.elm.nativeElement.tagName);
}
//TODO: OK to 'RE-ASSIGN' window.onresize ?
window.onresize = () => {
if (scrollContent) {
setTimeout(() => {
let elmHeight = scrollContent.clientHeight;
if (elmClickedY > elmHeight) {
let toScroll = elmClickedY - elmHeight;
scrollContent.scrollTop += toScroll + 40;
this.device.doTick();
}
}, 100);
}
}
}
}
4) Add this service and directive to you module
5) Use the kb-scroll directive on your <ion-content> HTML tag:
<ion-content kb-scroll>
... your form here ...
</ion-content>
6) now comes the really dirty part (and it will become clear why we need the service).
It seems that the repaint of the Ionic caret is triggered when something changes on the screen, so we need to force that.
I added a <span> to my HTML template that holds the root <ion-nav> tag:
app.html
<!-- invisible ticker, needed to get Keyboard caret into place -->
<div style="position: absolute; top:-100px">{{tickValue}}</div>
<!-- Disable swipe-to-go-back because it's poor UX to combine STGB with side menus -->
<ion-nav [root]="rootPage" #content swipeBackEnabled="false" class="root-content"></ion-nav>
7) Add this code in your app.component.ts:
#Component({
templateUrl: 'app.html'
})
export class MyApp {
#ViewChild(Nav) nav: Nav;
...
tickValue: string;
constructor(...,
public device: DeviceService) {
this.device.onTick.subscribe(tickValue => {
this.tickValue = tickValue;
});
}
So whenever the DeviceService#tickValue get's updated, this subscriber will update the app component's tick-value, causing a repaint of the screen, although the tick value is rendered outside the visible area of the screen. This seems to cause the Ionic caret to be positioned correctly.
Bonus: this also works for a textarea that takes more place than half the screen (= screen height - keyboard height). Just add the kb-scroll directive to your <textarea> HTML tag.
Please note this is a very hackish solution.
Any comments/improvements are really welcome!
I'm currently working in a simple app, which has a HTML section which uses a webview.
The contents are in an app/www folder, and I access them from a "home.ts" component with something like this in the HTML
<GridLayout
class="main-layout"
columns="*"
rows="*"
>
<WebView
src="~/www/index.html"
class="web-view"
col="0"
id="wv"
row="0"
></WebView>
</GridLayout>
This file uses a few images, some JS and some CSS.
It works fine on Android, but I can't make it work on iOS. Oh, and it works fine on the iOS emulator, but not in an actual device (I currently have a iOS 9 iPod touch for testing these things).
I have the correct keys configured on Info.plist (it works with URLs as https://www.google.com), and I think I'm not doing anything weird.
This is my package.json
{
"description": "WebView App",
"license": "LicenseRef-LICENSE",
"readme": "README",
"nativescript": {
"id": "com.app.name"
},
"dependencies": {
"#angular/animations": "~4.1.0",
"#angular/common": "~4.1.0",
"#angular/compiler": "~4.1.0",
"#angular/core": "~4.1.0",
"#angular/forms": "~4.1.0",
"#angular/http": "~4.1.0",
"#angular/platform-browser": "~4.1.0",
"#angular/router": "~4.1.0",
"nativescript-angular": "~3.0.0",
"nativescript-theme-core": "~1.0.2",
"reflect-metadata": "~0.1.8",
"rxjs": "~5.3.0",
"tns-core-modules": "^3.0.1",
"zone.js": "~0.8.2"
},
"devDependencies": {
"#angular/compiler-cli": "~4.1.0",
"babel-traverse": "6.24.1",
"babel-types": "6.24.1",
"babylon": "6.17.1",
"copy-webpack-plugin": "~4.0.1",
"lazy": "1.0.11",
"nativescript-css-loader": "~0.26.0",
"nativescript-dev-typescript": "~0.4.0",
"raw-loader": "~0.5.1",
"resolve-url-loader": "~2.0.2",
"tns-platform-declarations": "^3.0.0-rc.2",
"typescript": "~2.2.1"
}
}
It's a known bug for local pages, it's not able to determine what ~ means in terms of filesystem
you need to change the src in your code and figure out what ~refers to.
using :
import * as fs from "tns-core-modules/file-system";
public webViewSRC: string = encodeURI(`${fs.knownFolders.currentApp().path}/www/index.html`);
See this issue :
https://github.com/NativeScript/NativeScript/issues/4443