ProgressNS / nativescript-ui-feedback

This repository is used for customer feedback regarding Telerik UI for NativeScript. The issues system here is used by customers who want to submit their feature requests or vote for existing ones.
Other
115 stars 21 forks source link

RadListView - Going back to list view page after selection crashes the app #774

Closed Whip closed 6 years ago

Whip commented 6 years ago

Did you verify this is a real problem by searching the NativeScript Forum?

Yes. THe issue is similar to #350 and #271 but both says the issue has been fixed already

Tell us about the problem

I have a page where I display a list in grid view and multiple selections. User can select desired items and press Next and the ids of the selected items are passed to the next page as context. Now when you press back button the app crashes with following error

System.err: com.tns.NativeScriptException:
System.err: Calling js method canSelect failed
System.err:
System.err: Error: Item must be an object from the currently assigned source.
System.err: File: "file:///data/data/com.media.bwd.yashornaments/files/app/tns_modules/nativescript-ui-listview/ui-listview.js, line: 675, column: 12
System.err:
System.err: StackTrace:
System.err:     Frame: function:'RadListView.getViewForItem', file:'file:///data/data/com.media.bwd.yashornaments/files/app/tns_modules/nativescript-ui-listview/ui-listview.js', line: 675, column: 19
System.err:     Frame: function:'ListViewAdapter.canSelect', file:'file:///data/data/com.media.bwd.yashornaments/files/app/tns_modules/nativescript-ui-listview/ui-listview.js', line: 206, column: 41
...

If I don't select an item and press Next, then I come back, the page is fine. It seems like RadListView is trying to select the items even before they're loaded.

Which platform(s) does your issue occur on?

Android

Please provide the following version numbers that your issue occurs with:

Please tell us how to recreate the issue in as much detail as possible.

  1. Recreate the issue with this code
  2. Select Items and press next
  3. Use the back button to return to this page

Is there code involved? If so, please share the minimal amount of code needed to recreate the problem.

XML

<Page loaded="onLoaded" xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:lv="nativescript-ui-listview"
    xmlns:CheckBox="nativescript-checkbox" >

  <ActionBar title="Test" class="action-bar"></ActionBar>

  <ScrollView>
        <GridLayout rows="*,50">
          <StackLayout row="0" class="p-15">
                <Label text="PURITIES" class="bold m-b-10" />

                <lv:RadListView items="{{ purities }}" selectionBehavior="Press" multipleSelection="true" itemSelected="onItemSelected" itemDeselected="onItemDeselected" id="selectionList">
                    <lv:RadListView.listViewLayout>
                    <lv:ListViewGridLayout scrollDirection="Vertical" spanCount="2" />
                    </lv:RadListView.listViewLayout>

              <lv:RadListView.itemTemplate>
                <GridLayout rows="auto,auto" class="m-r-10 m-b-10 bg-trans">
                    <Image row="0" width="{{ width }}" height="{{ width }}" src="{{ image }}" />
                            <CheckBox:CheckBox row="0" checked="false" text="" verticalAlignment="top" horizontalAlignment="right" fillColor="#363636" id="{{ 'check'+id }}" isUserInteractionEnabled="false" />
                        <StackLayout row="0" verticalAlignment="bottom" class="cat-caption">
                            <Label text="{{ name }}" verticalAlignment="center" />
                        </StackLayout>
                    </GridLayout>
              </lv:RadListView.itemTemplate>
            </lv:RadListView>
            </StackLayout>
            <StackLayout row="1" orientation="horizontal" class="bg-secondary">
            <GridLayout columns="1*,1*">
                <GridLayout col="0" columns="*,40" class="bg-primary" height="100%" tap="selectAll">
                    <Label col="0" text="Select All" class="h3 text-black m-l-15" verticalAlignment="center" />
                    <CheckBox:CheckBox col="1" checked="false" text="" fillColor="#363636" id="selectAll" isUserInteractionEnabled="false" />
                </GridLayout>
                <GridLayout col="1" columns="*,40" height="100%" tap="toOrigin">
                    <Label col="0" text="Next" class="h3 m-l-15" verticalAlignment="center" />
                    <Label col="1" text="&#xe5c8;" class="mdi text-white h2" verticalAlignment="center" />
                </GridLayout>
            </GridLayout>
        </StackLayout>
    </GridLayout>
  </ScrollView>
</Page>

JS

const CheckBox = require('nativescript-checkbox');
const config = require("~/config.js");
const frame = require("ui/frame");
var functions = require("~/shared/functions.js");
const observableModule = require("data/observable");
const observableArrayModule = require("data/observable-array");
const http = require("http");
const settings = require("application-settings");
const view = require("ui/core/view");

const screenWidth = settings.getNumber("screenWidth");
var boxWidth = screenWidth/2-20;
var page;
var prodSearchCriteria;
var purityVM;
var purities;
var selectionList;
var topMost = frame.topmost();

exports.onLoaded = function(args) {
  page = args.object;

  purityVM = new observableModule.fromObject({
    userAvatar: settings.getString("userAvatar"),
        userEmail: settings.getString("userEmail")
  });
    purities = new observableArrayModule.ObservableArray();

  page.bindingContext = purityVM;
  prodSearchCriteria = page.navigationContext;
    purityVM.set("purities", purities);
  console.log(prodSearchCriteria);
  http.request({
    url: config.apiUrl+"purities.php",
    method: "POST",
    headers: { "Content-Type": "application/json" },
    content: JSON.stringify({
      auth: config.apiAuth,
      category: prodSearchCriteria.category
    })
  }).then(function(request){
    var response = JSON.parse(request.content);
    for (var i = 0; i < response.data.length; i++) {
        purities.push({
            id: response.data[i].id,
            name: response.data[i].name,
            image: config.siteUrl+"/img/icons/"+response.data[i].icon,
            width: boxWidth
        });
    }
    // console.log(purities);
    }, function(error){
      console.log(JSON.stringify(error));
    });

  selectionList = view.getViewById(page, "selectionList");
}

function updateCheckBoxes(){
  var selectedItems = selectionList.getSelectedItems();
  var selectedItemsString = "";

  for (var i = 0; i < selectedItems.length; i++) {
    selectedItemsString += selectedItems[i].id+",";
  }
  selectedItemsString = selectedItemsString.slice(0, -1);
  prodSearchCriteria.purities = selectedItemsString;

}
exports.onItemSelected = updateCheckBoxes;
exports.onItemDeselected = updateCheckBoxes;

exports.toOrigin = function(args){
    var navigationEntry = {
        moduleName: "origin/origin-page",
        animate: true,
        context: prodSearchCriteria
    }
    topMost.navigate(navigationEntry);
}

Please use arbitrary values where there isn't one available. Like I can't share the config.apiAuth here

tsonevn commented 6 years ago

Hi @VeeK727,

We investigated the project and found that the issue in your case is related to the fact that the items in the RadListView have been removed and added on every page loaded event. And regarding that, the ListView will try to restart the state of the selected items, before the items have been loaded. To solve your case, I would suggest to set up a flag which will set up the request to be executed on the initial page load. For example:

let firstload = true;
exports.onLoaded = function(args) {
  page = args.object;
if(firstload){
  purityVM = new observableModule.fromObject({
    userAvatar: settings.getString("userAvatar"),
        userEmail: settings.getString("userEmail")
  });
    purities = new observableArrayModule.ObservableArray();

  page.bindingContext = purityVM;
  prodSearchCriteria = page.navigationContext;
    purityVM.set("purities", purities);
  console.log(prodSearchCriteria);
  http.request({
    url: config.apiUrl+"purities.php",
    method: "POST",
    headers: { "Content-Type": "application/json" },
    content: JSON.stringify({
      auth: config.apiAuth,
      category: prodSearchCriteria.category
    })
  }).then(function(request){
    var response = JSON.parse(request.content);
    for (var i = 0; i < response.data.length; i++) {
        purities.push({
            id: response.data[i].id,
            name: response.data[i].name,
            image: config.siteUrl+"/img/icons/"+response.data[i].icon,
            width: boxWidth
        });
    }
    // console.log(purities);
    }, function(error){
      console.log(JSON.stringify(error));
    });

  selectionList = view.getViewById(page, "selectionList");
}
firstload=false;
}
Whip commented 6 years ago

That does work ... but kinda. I can come back to the page now but if I go to home page > categories > purities. THe page is loaded blank. It seems the flag is still set to false, but the data is gone.

Edit: Is there a way to learn whether user is clicking on items in the app to "move forwards" or using back button to "move backwards"? I'm having other issues too which could be solved with this, like product list page is not saving filters when you go to detail page and back because everything gets loaded like a new page.

tsonevn commented 6 years ago

Hi @VeeK727, In Android, you can capture the tap on the back button. Check out the article here. You can also check if some of the page events will help you to solve your case navigatedTo, navigatingTo, navigatedFrom, navigatingFrom - https://pointdeveloper.com/nativescript-page-navigation-events/

Whip commented 6 years ago

Thank you very much for the article. I've been looking for this since I started my first NS project. This article doesn't come up in my search, instead I've been using this weird code which I can't even understand. Using the code in your linked article, I set a boolean in application-settings to true and I run my code only when this code is false. Here's the code if someone else need this. app.js

function backEvent(args) {
    var currentPage = frame.topmost().currentPage;
    if (currentPage && currentPage.exports && typeof currentPage.exports.backEvent === "function") {
         currentPage.exports.backEvent(args); //if you need to take a custom action on one or some pages you can define backEvent function on that page and it will be executed. Otherwise code in 'else' will be executed
    } else {
         settings.setBoolean("comingBack", true);
    }
}

On every page

exports.onLoaded = function(args){
   if(settings.getBoolean("comingBack") == false){
        // load your page, run your code
   } else {
        settings.setBoolean("comingBack", false); // when you come back to this page, set this boolean to false, so any pages you further visit will load. If you pressed back again, this will be set to true by app.js code
   }
}