4

I'm trying to create a $ionicPopup where one of the buttons is disabled under certain conditions (being the return value of a function, let's call it MyFunction()). I want to use ng-disabled for this purpose.

The problem is, I don't know how to programmatically add the attribute "ng-disabled".

What I tried so far:

  • Adding the attribute when creating the popup, like attr:"ng-disabled='myFunction()'"
  • Adding the attribute after the popup was created, using JavaScript => The problem is that the setAttribute() method is executed before the popup is actually shown, so I would need a way to detect when the popup is open, and execute the method only then.
  • Creating the button as html elements inside the popup template, and not setting any button with the $ionicPopup.show() method. This works, but I'm not satisfied with it because I don't want to "reinvent the wheel" and redefine CSS styles for buttons that are already covered by Ionic framework.

My JS function:

$scope.displayPopUp=function(){
    var alertPopup = $ionicPopup.show({
        templateUrl: 'sharePopUp.html',
        title: 'Invite a friend',
        cssClass: 'popupShare',
        buttons:[
            {
                text:'Close',
                type: 'button-round button-no',
                onTap: function(){
                        /* Some instructions here */
                    }
            },
            { /* v THIS IS THE BUTTON I WANT TO DISABLE UNDER CERTAIN CONDITIONS v */
                text:'Share',
                type: 'button-round button-yes',
                onTap: function(){
                       /* Some instructions here */
                    }
            }
        ]
    }); 

    $(".button-yes")[0].setAttribute("ng-disabled", "MyFunction()"); /* NOT WORKING BECAUSE button-yes IS NOT EXISTING YET */
}
Reyedy
  • 918
  • 2
  • 13
  • 36
  • Not nice but try with a setTimer – Mario Padilla Apr 17 '18 at 10:25
  • I tried to do that, and it gives the button the ng-disabled attribute, but it's not working the way I would like, an even after a $compile(element)($scope), it even makes the button disappear... – Reyedy Apr 18 '18 at 16:48

2 Answers2

3

TL;DR

$timeout(function () {                              // wait 'till the button exists
  const elem = $('.button-yes')[0];
  elem.setAttribute('ng-disabled', 'MyFunction()'); // set the attribute
  $compile(elem)(angular.element(elem).scope());    // Angular-ify the new attribute
});

Live demo: working plunk

Introduction

That problem you're encountering, it's a real one, and it has apparently been for years.

Here's the latest version of the code used by $ionicPopup (last updated in December 2015)

This template is the one used by your Ionic-1 popups (from the first lines of the code linked above):

var POPUP_TPL =
  '<div class="popup-container" ng-class="cssClass">' +
    '<div class="popup">' +
      '<div class="popup-head">' +
        '<h3 class="popup-title" ng-bind-html="title"></h3>' +
        '<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
      '</div>' +
      '<div class="popup-body">' +
      '</div>' +
      '<div class="popup-buttons" ng-show="buttons.length">' +
        '<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
      '</div>' +
    '</div>' +
  '</div>';

There's one line in particular that's interesting to us: the button template:

<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>

As you can see, there's just no built-in way to alter its button's attributes.

Two approaches

From here, you've got two fixes:

  1. We can contribute to their project on GitHub, implement the missing functionality, write the tests for it, document it, submit an issue, a Pull Request, ask for a newer version to be released and use the newer version.

This is the ideal solution, 'cause it fixes everyone's problems forever. Although, it does take some time. Maybe I'll do it. Feel free to do it yourself though, and tag me, I'll +1 your PR

  1. Write a dirty piece of code that monkey-patches your specific problem in your specific case

This isn't ideal, but it can be working right now.

I will explore and expand on the (quick 'n dirty) option #2 below.

The fix

Of the 3 things you've tried so far:

  • the first one is simply not a thing (although it could be if we implement it, test it, document it and release it)
  • the third one is rather unmaintainable (as you know)

That leaves us with the second thing you mentioned:

Adding the attribute after the popup was created, using JavaScript

The problem is that the setAttribute() method is executed before the popup is actually shown, so I would need a way to detect when the popup is open, and execute the method only then.

You're right, but that's only part one of a two-fold problem.

Part 1: The button isn't created yet

Actually, you can delay that call to setAttribute to later, when the popup is shown. You wouldn't wanna delay it by any longer than would be noticeable by a human, so you can't reasonably go for anything longer than 20ms.
Would there be some callback when the popup is ready, we could use that, but there isn't.

Anyways, I'm just teasing you: JavaScript's "multi-tasking" comes into play here and you can delay it by 0 millisecond!
In essence, it has to do with the way JS queues what it has to do. Delaying the execution of a piece of code by 0ms puts it at the end of the queue of things to be done "right away".

Just use:

setTimeout(function () {
  $(".button-yes")[0].setAttribute("ng-disabled", "MyFunction()");
}, 0); // <-- 0, that's right

And you're all set!

Well, you do have a button whose ng-disabled attribute indeed is "MyFunction()". But it's not doing anything... So far, you simply have an HTML element with an attribute that doesn't do anything for a simple HTML button: Angular hasn't sunk its teeth into your new DOM and hooked itself in there.

Part 2: Angular isn't aware of the new attribute

There's a lot to read here about this, but it boils down to the following: Angular needs to compile your DOM elements so that it sets things in motion according to your Angular-specific attributes.

Angular simply hasn't been made aware that there's a new attribute to your button, or that it should even concern itself with it.

To tell Angular to re-compile your component, you use the (conveniently named) $compile service.

It will need the element to compile, as well as an Angular $scope to compile it against (for instance, MyFunction probably doesn't exist in your $rootScope).

Use it once, like so:

$compile(/* the button */ elem)(/* the scope */ scope);

Assuming the following element is your button:

const elem = $(".button-yes")[0];

... you get its actual scope through its corresponding Angular-decorated element thingy:

const scope = angular.element(elem).scope();

So, basically:

const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()');
$compile(elem)(angular.element(elem).scope());

Tadaaa! That's it!
... sort of. Until there's some user interaction that would alter the corresponding $scope, the button is actually not even displayed.

Bonus Part: Avoid $scope.$apply() or $scope.$digest()

Angular isn't actually magically picking up things changing and bubbling it all to the right places. Sometimes, it needs to explicitly be told to have a look around and see if the elements are in sync with their $scope.

Well, more specifically, any change that happens asynchronously won't be picked up by itself: typically, I'm talking about AJAX calls and setTimeout-delayed functions. The methods that are used to tell Angular to synchronise scopes and elements are $scope.$apply and $scope.$digest... and we should thrive on avoiding them :)

Again, there's lots of reading out there about that. In the meantime, there's an Angular service (again), that can (conceptually, it's not the literal implementation) wrap all your asynchronous code into a $scope.$apply() -- I'm talking about $timeout.

Use $timeout instead of setTimeout when you will change things that should alter your DOM!

Summing it all up:

$timeout(function () {                              // wait 'till the button exists
  const elem = $('.button-yes')[0];
  elem.setAttribute('ng-disabled', 'MyFunction()'); // set the attribute
  $compile(elem)(angular.element(elem).scope());    // Angular-ify the new attribute
});

Live demo: working plunk

Community
  • 1
  • 1
ccjmne
  • 9,333
  • 3
  • 47
  • 62
  • Thanks a lot for your answer, it's very clear and pedagogic and your working example is amazing. You're the real MVP. I tried to implement it on my actual code, and the button doesn't display (I also tried to change angular.element(elem).scope() into $scope because I had a ng:areq error). If I comment the line where I $compile it, it displays it (but not disabled of course). Do you think there might be something up with the compiling part? Or maybe the MyFunction() itself, which is a function inside the same controller as the $scope.displayPopUp() one? – Reyedy Apr 23 '18 at 08:26
  • @Driblou: I'm glad you appreciate my answer! You actually cannot use `$scope` in here ; it doesn't reference exactly the same `$scope` used by your popup. At [line 293 of the source code](https://github.com/ionic-team/ionic-v1/blob/master/js/angular/service/popup.js#L293), we see that they create and use a `$new` scope for the popup, merely inheriting from the one you supply. The scope you supply **doesn't have any `buttons`** property; so, when you compile your button (` – ccjmne Apr 23 '18 at 13:43
  • @Driblou: You do need the effective scope. I just read through the entire source code and there's no way to obtain it other than `angular.element(elem).scope()`. Ionic-1 is not perfect! You could also just duplicate the `buttons` property you pass into your `$scope`, but you'd eventually run into deeper problems later on. I think it's best to find out why you get an error when using `angular.element(elem)`... Could you give me a link to a project (on Plunker for instance) that reproduces the problem? – ccjmne Apr 23 '18 at 13:54
2

I think in ionic v1 Ionic Framework team have not implemented this yet as per (Oct 6, '14 10:49 PM). I think still situation is same. But there is a work around for that.

Option 1:

What I understand from your question, your main purpose is to prevent user to click on buttonDelete ionicPopup buttons and perform some instructions until MyFunction() returns truecreate your own template with buttons which you can fully control them. Below is code:

You can achieve this inside onTap :. Here you can add condition of your MyFunction() like below:

JavaScript:

// Triggered on a button click, or some other target
$scope.showPopup = function() { 

  // Enable/disable text"Share" button based on the condition
  $scope.MyFunction = function() {
    return true;
  };

  //custom popup
  var myPopup = $ionicPopup.show({
    templateUrl: 'Share'"popup-template.html",
    typetitle: 'button-round"Invite button-yes'a friend",
    onTapscope: function(e)$scope
 { });

  // close popup on Cancel ifbutton (MyFunctionclick
  $scope.closePopup = function()) {
    myPopup.close();
  };

};

HTML:

  /*<button Someclass="button instructionsbutton-dark" hereng-click="showPopup()">
 */     show
    </button>

  }<script elseid="popup-template.html" {type="text/ng-template">
    <p>Share button is disabled if condition not /satisfied</don'tp>
 allow the user to<button performclass="button unlessbutton-dark" MyFunctionng-click="closePopup()"> returns 
 true     Cancel
    </button>
    e.preventDefault<button class="button button-dark" ng-disabled="MyFunction(); == true"> 
      }Share
    }</button>
  }</script>

Working example here Here is working codepen snippet:

https://codepen.io/anon/pen/bvXXKG?editors=1011

Option 2:

Delete ionicPopup buttons and create your own template with buttons which you can fully control them. Below is code:

JavaScript:

// Triggered on a button click, or some other target
$scope.showPopup = function() {

  // Enable/disable "Share" button based on the condition
  $scope.MyFunction = function() {
    return true;
  };

  //custom popup
  var myPopup = $ionicPopup.show({
    templateUrl: "popup-template.html",
    title: "Invite a friend",
    scope: $scope
  });

  // close popup on Cancel button click
  $scope.closePopup = function() {
    myPopup.close();
  };

};

HTML:

  <button class="button button-dark" ng-click="showPopup()">
      show
    </button>

  <script id="popup-template.html" type="text/ng-template">
    <p>Share button is disabled if condition not satisfied</p>
    <button class="button button-dark" ng-click="closePopup()"> 
      Close
    </button>
    <button class="button button-dark" ng-disabled="MyFunction() == true"> 
      Share
    </button>
  </script>

Here is working codepen snippet:

https://codepen.io/anon/pen/qYEWmY?editors=1010

Note: Apply your own styles/button's alignment etc

I hope it will help you.

Vikasdeep Singh
  • 20,983
  • 15
  • 78
  • 104
  • I think this would allow me to avoid error cases, but what I would like to do is to actually disable the button, meaning that you can't click on it and it has the "disabled" attribute and style. – Reyedy Apr 18 '18 at 15:30
  • Thank you for your answer. I know this works, but as I stated in my question, this is a solution I would like to avoid because I don't want to rewrite the exact same css as already defined in Ionic, I don't believe that's a good practice. However, if that's the only solution, I guess I will eventually do that. – Reyedy Apr 19 '18 at 13:31
  • I do appreciate your efforts and I think your solutions are interesting, but I don't see what upvoting has to do with courtesy. I'm not satisfied with such a workaround, because it's already something I tried before asking the question here. Thank you for your help anyway, I appreciate you taking the time to dig into it. – Reyedy Apr 19 '18 at 13:50