Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SelectModel plugin async data loading #298

Closed
rochdi80tn opened this issue May 21, 2019 · 11 comments · Fixed by #612
Closed

SelectModel plugin async data loading #298

rochdi80tn opened this issue May 21, 2019 · 11 comments · Fixed by #612

Comments

@rochdi80tn
Copy link
Contributor

The current implementation of SelectModel allows early loading of the dropdown options. What might be a useful feature is to be able to do asynchronous data loading of items using API calls both initially after the select is being rendered and later when user do search an entry in the list.

Goals and benefits

Many benefits we will gain :

  • optimize the data transfer for large tables, we can add an option to limit the number of returned entries
  • always fetch latest data for applications with high rate of data updates
  • add server checks eg. inorder to filter out the returned results as per current user rights
  • send custom search queries using data form other inputs , usefull for related dropdown lists

Open for discussion

@nadar
Copy link
Member

nadar commented May 21, 2019

send custom search queries using data form other inputs , usefull for related dropdown lists

How would such an implementation look like?

@rochdi80tn
Copy link
Contributor Author

We can extend the ngRestAttributeTypes() where clause for instance to add reference to another field.. something like this example : (on an history transaction for books)
image

Or we let the user using some call back function on the author select dropdown list change event, do a manual call to upate the books dropdown list via async request.

@rochdi80tn
Copy link
Contributor Author

this is a work in progress
I am trying to implement this on a project and I am working directly into adding a new directive I called zaa.directive("zaaSelect2", function() {} using the exisiting zaaSelect as starting point and using events.. once this is stable enough I'll try to integrate it into the selectModel a plugin.. or maybe to a brand new one

This works pretty well so far..
So just an insight of what I am doing in case someone may suggest some thing...

 
           // new scope optins
            "related": "@relatedto",
            "optionsapi": "@optionsapi"
 ...
  
         
...
            /* listeners */
           // add a listener  to an event the related model change event that will be fired when a change 
           // occurs in the related model 
            if ($scope.related != undefined) {
                $scope.$on( $scope.related+'Change', function(event, n, o) {
                    //console.log($scope.id +   ' RECEIVED event '+$scope.related+'Change = '+ n + 'o='+o);
                    if ( o != undefined){
                        $scope.model = undefined;
                    }
                    $scope.getAPIOptions(n);                                     
                });                
            } 



             // fire the  new change event if new value is different from old one
            $timeout(function(){
                $scope.$watch(function() { return $scope.model }, function(n, o) {
                    if (n == undefined || n == null || n == '') {
...
                    }
                    else {
                        var exists = $scope.valueExistsInOptions(n);
                        if (!exists && n != o) {
                            $scope.model = n;
                        }
                        console.log($scope.id + ' BROADCAST event '+$scope.id+'Change ='+ n + ' old='+o)
                        $rootScope.$broadcast($scope.id+'Change', $scope.model, o);
                        
                    }
...                    
  

          // add new method to load options from an API endpoint defined in the options  
            $scope.getAPIOptions = function(relatedID) {
                    if ($scope.optionsapi == undefined)
                        return;
                    
                    var url =$scope.optionsapi
                     
                    if (relatedID != undefined ){
                        url = url + '?related_id=' + relatedID;;
                    }
                    //console.log('$http.get('+url+')');
 
                    $http.get( url ).then(
                        function(success) {
                            
                            //console.log('$http.then:success.data('+url+')='); console.log(success.data);

                            $scope.options =success.data                            
                            angular.forEach($scope.options, function(item) {
                                item[$scope.optionslabel] = decodeEntities ( item[$scope.optionslabel] );  
                            });
                    }, function(response){
                        console.log('response=');console.log(response);
                    });
            };
            

           // initial load of optins
            $scope.getAPIOptions();
           

and from calling side :

<zaa-select-2 id="author_id_select" 
              fieldid="author_id_select" 
              label="<?= Module::t('author_id_select'); ?>" 
              model="settings.author" 
              zaa-options="" 
              optionsapi="admin/api-author/list"
              optionslabel="name" 

              optionsvalue="id"  />
<zaa-select-2 fieldid="book_id_select" 
              label="<?= Module::t('book_id_select'); ?>" 
              model="settings.book" 
              zaa-options="" 
              optionsapi="admin/api-book/clist"
              optionslabel="year" 
              optionsvalue="id" 
              relatedto="author_id_select"
            />                 

@rochdi80tn
Copy link
Contributor Author

Had to call a decodeEntities function on all labels as the returned ones are HTML encoded for french letters like é ... so might be this should be fixed already in the current selectModel implementation (minor bug)

angular.forEach($scope.options, function(item) {
      item[$scope.optionslabel] = decodeEntities ( item[$scope.optionslabel] );  
});


var decodeEntities = (function() {
  // this prevents any overhead from creating the object each time
  var element = document.createElement('div');

  function decodeHTMLEntities (str) {
    if(str && typeof str === 'string') {
      // strip script/html tags
      str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gmi, '');
      str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, '');
      element.innerHTML = str;
      str = element.textContent;
      element.textContent = '';
    }

    return str;
  }

  return decodeHTMLEntities;
})();

@nadar
Copy link
Member

nadar commented May 28, 2019

I have not deeply checked your code, but the most important is that the angular plugin can watch the value of a certain field, and if this value changes it needs to send an xhr request with the value. So now the problem is: who is communicating with the plugin? there is no controller or something like this, maybe we need a new action in the ngrest base api. Another solution would to provide all informations and just filter them out.

@nadar nadar self-assigned this Dec 18, 2019
@nadar
Copy link
Member

nadar commented Dec 18, 2019

An async option to load makes sense, maybe it it should add another plugin which just opening an url which must be configured:

selectAsyncList('apth/to/api')

@nadar nadar removed their assignment Apr 22, 2020
@flashjay
Copy link

https://demos.krajee.com/widget-details/depdrop @nadar
this feture stop me many days using luya.....

@flashjay
Copy link

@rochdi80tn could show u completely solution on this feture??

@nadar
Copy link
Member

nadar commented Jun 23, 2020

hey @flashjay - I totally understand the need and request for such a plugin which @rochdi80tn mentioned.

I am going to prioritize this issue higher now. I think we can achieve this but going step by step:

  1. We need to create a new plugin which can just talk to an api.
  2. We need to adjust the plugin to dynamically update properties and send those to the given api.

@nadar nadar self-assigned this Jun 23, 2020
@flashjay
Copy link

flashjay commented Jun 23, 2020

This code work for me. but, ..

/**
 * @since 1.0.0
 */
class DepSelect extends Select
{
    public $api = '';

    public $dep = '';

    /**
     * {@inheritDoc}
     */
    public function getData()
    {
        return [];
    }

    /**
     * @inheritdoc
     */
    public function renderList($id, $ngModel)
    {
        return $this->createListTag($ngModel);
    }

    /**
     * @inheritdoc
     */
    public function renderCreate($id, $ngModel)
    {
        return [
            $this->createFormTag('zaa-select-2', $id, $ngModel, ['dep' => $this->dep, 'api' => $this->api, 'type' => 'create'])
        ];
    }

    /**
     * @inheritdoc
     */
    public function renderUpdate($id, $ngModel)
    {
        return [
            $this->createFormTag('zaa-select-2', $id, $ngModel, ['dep' => $this->dep, 'api' => $this->api, 'type' => 'update'])
        ];
    }
}

Js

zaa.directive("zaaSelect2", function () {
    return {
        restrict: 'E',
        transclude: false,
        scope: {
            "model": "=",
            "label": "@label",
            "i18n": "@i18n",
            "id": "@fieldid",
            "initvalue": "@initvalue",
            "dep": "@dep",
            "api": "@api",
            "type": "@type",
        },
        controller: ['$scope', '$http', 'AdminToastService', function ($scope, $http, AdminToastService) {
            $scope.$watch(function () {
                return $scope.$parent.data[$scope.type][$scope.dep];
            }, function (n, o) {
                if (n == undefined || n == '') return;
                var url = $scope.api + '?' + $scope.dep + '=' + n;
                $http.get(url).then(function (success) {
                    $scope.options = success.data.list;
                }, function (error) {
                    AdminToastService.errMsg(error.data);
                });
            })
        }],
        template: function () {
            return '<div class="form-group form-side-by-side" ng-class="{\'input--hide-label\': i18n}">' +
                '<div class="form-side form-side-label">' +
                '<label for="{{id}}">{{label}}</label>' +
                '</div>' +
                '<div class="form-side">' +
                '<luya-select ng-model="model" options="options" id="{{id}}" initvalue="{{initvalue}}" />' +
                '</div>' +
                '</div>';
        },
    }
});

@nadar
Copy link
Member

nadar commented Jan 21, 2021

Hey @rochdi80tn and @flashjay i have created a new plugin which can make requests to an api and return a result, it also can interact with the attributes around (for example, whenever an other attribute changes the value can be passed as argument to an api).

Please take a look at #612 and https://github.com/luyadev/luya-module-admin/blob/master/src/ngrest/plugins/SelectAsyncApi.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants