Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 25, 2014 @ 11:06
    Warren Buckley
    0

    V7 Custom PreValue with custom Validation

    Hello all,
    I am currently building an Umbraco package for a YouTube property editor.

    One of the PreValues is a custom and allows the user to enter a username along with a button to query the username/channel on YouTube.

    Currently if the username does not exist I use the Umbraco AngularJS NotificationsService to show an error message that the user could not be found.

    However it is still possible to save the DataType with this custom PreValue in.
    What I would like to do is to prevent the datatype from being saved and giving the user a validation message that they must set a username.

    A simple textbox and a required attribute is simply not enough for me, as I am not saving the value of the textbox as the prevalue data in $scope.model.value but instead a JSON object including the channel username/title, channel description, channel thumbnail image & statistics.

    Here is my AngularJS Controller for my PreValue for reference:

    angular.module("umbraco").controller("YouTube.prevalue.channel.controller", function($scope, YouTubeResource, notificationsService) {
    
    //Set to be default empty object or value saved if we have it
    $scope.model.value = $scope.model.value ? $scope.model.value : null;
    
    if ($scope.model.value) {
        //Have a value - so lets assume our JSON object is all good
        //Debug message
        console.log("Scope Model Value on init", $scope.model.value);
    
        //As we have JSON value on init
        //Let's set the textbox to the value of the querried username
        $scope.username = $scope.model.value.querriedUsername;
    }
    
    
    $scope.queryChannel = function(username) {
    
        //Debug info
        console.log("Query Channel Click", username);
    
        //Query this via our resource
        YouTubeResource.queryUsernameForChannel(username).then(function(response) {
    
            //Debug info
            console.log("Value back from query API", response);
            console.log("Items length", response.data.items.length);
    
    
            //Only do this is we have a result back from the API
            if (response.data.items.length > 0) {
                //Data we are interested is in
                //response.data.items[0]
                var channel = response.data.items[0];
    
    
                //Create new JSON object as we don't need full object from Google's API response     
                var newChannelObject = {
                    "querriedUsername": username,
                    "channelId": channel.id,
                    "title": channel.snippet.title,
                    "description": channel.snippet.description,
                    "thumbnails": channel.snippet.thumbnails,
                    "statistics": channel.statistics
                };
    
                //Set the value to be our new JSON object
                $scope.model.value = newChannelObject;
            } else {
                //Fire a notification - saying user can not be found
                notificationsService.error("YouTube User Lookup", "The channel/user '" + username + "' could not be found on YouTube");
    
                //Set the value to be empty
                $scope.model.value = null;
            }
        });
       };
    });
    

    If anyone can offer any pointers or advice on how best to do this, that would be fantastic.

    Cheers,
    Warren

  • Matthew Wise 271 posts 1373 karma points MVP 5x c-trib
    Jul 25, 2014 @ 11:30
    Matthew Wise
    0

    Not 100% but I think you can add a event handler to the submit of the form do you custom validation if successful the form submit would continue as normal.

  • Kasper Holm 47 posts 180 karma points
    Jul 25, 2014 @ 14:03
    Kasper Holm
    0

    Hi warren

    have you look into a custom Directives, thats how i ended up making my custom validation ? :)

    https://docs.angularjs.org/guide/directive

  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 25, 2014 @ 15:18
    Warren Buckley
    0

    Hiya.
    Thanks for the replies. Kasper do you have an example with a directive & custom validation please or how you would go about approaching my problem.

    Matthew, do you have an example of this event I can listen to with an .On() event hook function, to do what I need to?

    Thanks,
    Warren

  • Kasper Holm 47 posts 180 karma points
    Jul 25, 2014 @ 15:46
    Kasper Holm
    1

    Yes i do, this is added on my controller, and it basicly just checks that if there is adeed any items do the value :)

    .directive('daysAdded', function () {

        return {
            require: 'ngModel',
            link: function(scope, elm, attrs, ctrl) {
                ctrl.$formatters.unshift(function(viewValue) {
                    if (scope.model.value.length > 0) {
                        ctrl.$setValidity('daysAddedError', true);
                        return viewValue;
                    } else {
                        ctrl.$setValidity('daysAddedError', false);
                        return undefined;
                    }
                });
    
            }
        };
    });
    });
    
  • Comment author was deleted

    Jul 25, 2014 @ 16:21

    +1 to ==^ answer

  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 25, 2014 @ 16:30
    Warren Buckley
    0

    So go with a custom directive is the consensus? I thought if the Umbraco guys are using a form I can just set the validity or am I totally wrong with this?!

    formName.inputName.$setValidity("YouTubeChannel", false);
    

    Cheers,
    Warren

  • Comment author was deleted

    Jul 25, 2014 @ 16:34

    Just to add on to Twitter comments...

    You don't have to validation via a directive, rather hooking into the formatters/parsers to do validation is the idea.  Set the validity and the rest should just work.

    https://github.com/imulus/Archetype/blob/master/app/directives/archetypeproperty.js#L153

  • Kenn Jacobsen 133 posts 791 karma points MVP 4x c-trib
    Jul 26, 2014 @ 09:18
    Kenn Jacobsen
    0

    Hiya all,

    I'm all for directives, they work great on client side validation and you can write them super generic to reuse them across your views. However, I think you'll find yourself in a pickle trying to do what you're trying to do with a directive. Why? Because formatters, parsers, watches and the like all fire when the value changes. You don't want that, because then you'll end up querying the YouTube resource for each letter typed in your text input. I'm guessing that's also why you have your "validate" button.

    I think you'll have to roll with some controller logic for this one. I'd like to be proven wrong on this point (please, someone?), but generally when I have to validate multiple fields against each other or do some server side validation with $http, I almost always find that directives either fail me or plain simply just over-complicates the code without giving me the benefit of reuse-ability, and I have to revert back to using controller logic.

    As for preventing the users from saving an invalid username/channel configuration, if everything else fails you can always use the angularHelper to invalidate the entire form like this:

    angular.module("umbraco").controller("Something.Config.Controller",
      function($scope, angularHelper) {
        // ...
        $scope.validate = function() {
          var isValid = $scope.model.value.somethingToValidate != null
            && $scope.model.value.somethingToValidate != ""
            && $scope.model.value.somethingToValidate != "invalid";
    
          angularHelper.getCurrentForm($scope).$setValidity('somethingToValidate', isValid);
        }
        // ...
      });
    

    It's hacky, but it'll work if you can't get to the input control(s) that are invalid.

    -Kenn

  • Comment author was deleted

    Jul 26, 2014 @ 13:59

    @Kenn,

    I don't disagree with your reasoning, but couldn't you just simply check a flag before sending a request to YouTube?

    i.e.

    function validate(){

       if(!alreadyValidated){

         //check YouTube

        alreadyValidated = true; 

      }

    }

  • Kenn Jacobsen 133 posts 791 karma points MVP 4x c-trib
    Jul 26, 2014 @ 14:20
    Kenn Jacobsen
    0

    @Kevin yuss you could do that, but wouldn't you still end up sending X requests to YouTube before you hit a positive validation result?

    In this particular case I guess one could also add an onBlur directive to the mix and use the blur event from the input field to perform the validation in a controller method (ng-blur doesn't work in the version of Angular used in the Umbraco backend, but I have a directive implementation of onBlur and onFocus somewhere, if anyone wants it).

  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 26, 2014 @ 20:23
    Warren Buckley
    0

    Kenn I have considered the onblur of the field to do the YouTube lookup & validation at the same time. I am not 100% keen on this and like the action of the user confirming they want to check YouTube for that username.

    So the entire form invalidation may be the best, or would it be best to do a directive in replace of ng-click on the I have, that does the same for me instead?

    Cheers,
    Warren :)

  • Comment author was deleted

    Jul 26, 2014 @ 20:50

    @Kenn good observation.

    @Warren

    How about a non-bound textbox that takes a username.  Click a button (which fires a youtube request).  Then if successful, save a boolean to the model a value that says the the youtube was valid. Use parsers/formatters to check boolean to validate.  Save username as well to the model if ya like.

  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 26, 2014 @ 20:59
    Warren Buckley
    0

    @Kevin that does sound a good solution, but as you just have replied I have a solution that is working and I am keen to see what everyone thinks of this please.

    angular.module("umbraco").controller("YouTube.prevalue.channel.controller", function ($scope, YouTubeResource, notificationsService, angularHelper) {
    
    //Set to be default empty object or value saved if we have it
    $scope.model.value = $scope.model.value ? $scope.model.value : null;
    
    if($scope.model.value){
        //Have a value - so lets assume our JSON object is all good
        //Debug message
        console.log("Scope Model Value on init", $scope.model.value);
    
        //As we have JSON value on init
        //Let's set the textbox to the value of the querried username
        $scope.username = $scope.model.value.querriedUsername;
    }
    
    
    $scope.queryChannel = function(username) {
    
        //Debug info
        console.log("Query Channel Click", username);
    
        //Default flag for validity
        var isThisValid = false;
    
        //Query this via our resource
        YouTubeResource.queryUsernameForChannel(username).then(function(response) {
    
            //Debug info
            console.log("Value back from query API", response);
            console.log("Items length", response.data.items.length);
    
    
            //Only do this is we have a result back from the API
            if(response.data.items.length > 0){
                //Data we are interested is in
                //response.data.items[0]
                var channel = response.data.items[0];
    
    
                //Create new JSON object as we don't need full object from Google's API response     
                var newChannelObject = {
                    "querriedUsername": username,
                    "channelId": channel.id,
                    "title": channel.snippet.title,
                    "description": channel.snippet.description,
                    "thumbnails": channel.snippet.thumbnails,
                    "statistics": channel.statistics
                };
    
                //Set the value to be our new JSON object
                $scope.model.value = newChannelObject;
    
                //Set our flag to true
                isThisValid = true;
            }
            else {
                //Fire a notification - saying user can not be found
                 notificationsService.error("YouTube User Lookup","The channel/user '" + username + "' could not be found on YouTube");
    
                 //Set the value to be empty
                 $scope.model.value = null;
    
                 //Ensure flag is set to false
                 isThisValid = false;
            }
    
    
            //Get the form with Umbraco's helper of this $scope
            //The form is wrapped just around this single prevalue editor
            var form = angularHelper.getCurrentForm($scope);
    
            //Inside the form we have our input field with the name/id of username
            //Set this field to be valid or invalid based on our flag
            form.username.$setValidity('YouTubeChannel', isThisValid);
    
            //Debug
            console.log("Form", form);
            console.log("Form Username", form.username);
            console.log("Is this Valid?", isThisValid);
    
        });     
    
    };
    });
    
  • Kenn Jacobsen 133 posts 791 karma points MVP 4x c-trib
    Jul 27, 2014 @ 09:26
    Kenn Jacobsen
    0

    Hi Warren.

    Your solution will surely work. What bugs me a little bit about it is the line form.username.$setValidity('YouTubeChannel', isThisValid);. It implies that your controller knows about the structure/implementation of your form/markup.

    Then again, the alternative is a fair bit of extra code just to be rid of this binding, it might not really be worth it. But for arguments sake, here's an example of an alternative solution, where the directive performs validation of the form element it's bound to.

    Controller:

    angular.module("umbraco").controller("Something.Config.Controller",
      function ($scope, angularHelper) {
        // set the model for the input field
        $scope.model.somethingToValidate = $scope.model.value;
    
        $scope.validate = function () {
          // validate the input field value
          var isValid = $scope.model.somethingToValidate != null
            && $scope.model.somethingToValidate != ""
            && $scope.model.somethingToValidate != "invalid";
    
          // update the model value according to the validation result
          if (isValid) {
            $scope.model.value = $scope.model.somethingToValidate
          }
          else {
            $scope.model.value = null;
          }
    
          // debug info
          console.log("Controller says", isValid, $scope.model.value);
        }
      });
    

    Directive:

    angular.module("umbraco").directive("somethingValidate",
      function () {
        return {
          restrict: "A",
          require: "ngModel",
          link: function (scope, element, attr, ctrl) {
            ctrl.$parsers.unshift(function (viewValue) {
              return validateSomething(viewValue);
            });
            ctrl.$formatters.unshift(function (viewValue) {
              return validateSomething(viewValue);
            });
            scope.$watch("model.value", function (v) {
              // need to register a watch on model value to re-validate once the validated value changes on scope
              validateSomething(v);
            });
    
            function validateSomething(viewValue) {
              // it's valid if the view has a value and that value is equal to the currently validated value on scope
              var isValid = viewValue != null && scope.model.value == viewValue;
    
              // set the control validity accordingly
              ctrl.$setValidity("something", isValid);
    
              // debug info
              console.log("Directive says", isValid, scope.model.value);
    
              return viewValue;
            }
          }
        };
      });
    

    Usage:

    <div ng-controller="Something.Config.Controller">
      <input type="text" ng-model="model.somethingToValidate" something-validate />
      <br/>
      <a href ng-click="validate()">Validate</a>
    </div>
    

    An added value (or annoyance?) of this is that the field invalidates when the user changes the field value, and remains invalid until the controller has explicitly validated the field value.

  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 27, 2014 @ 22:19
    Warren Buckley
    0

    Hi Kenn, I see your point with this and may refactor to do this at some point.

    I followed the same pattern that the core guys have done for validating min & max for the content picker, that has hidden input fields in the view and sets their validity in the same way based on the form name and input name.

    https://github.com/umbraco/Umbraco-CMS/blob/7.1.5/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js#L98 https://github.com/umbraco/Umbraco-CMS/blob/7.1.5/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html#L30

    I need to do something similar for the main editor to ensure a user can select a min or max or both YouTube video items. Would it be best to replicate this or is the best idea to go with directives for everything for this?

    Thanks,
    Warren

    //Validate!
    if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) {
        $scope.contentPickerForm.minCount.$setValidity("minCount", false);
    }
    else {
        $scope.contentPickerForm.minCount.$setValidity("minCount", true);
    }
    
    if ($scope.model.config && $scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) < $scope.renderModel.length) {
        $scope.contentPickerForm.maxCount.$setValidity("maxCount", false);
    }
    else {
        $scope.contentPickerForm.maxCount.$setValidity("maxCount", true);
    }
    
  • Kenn Jacobsen 133 posts 791 karma points MVP 4x c-trib
    Jul 27, 2014 @ 22:45
    Kenn Jacobsen
    1

    Uh... blimey, I dunno :)

    From a purist point of view I'm sure you should use directives in an effort to eliminate the controller's knowledge of the view implementation. From a practical point of view, I'm not so sure. Both solutions have obvious drawbacks. Both solutions work. That being said, the code you've shown above is really the same code twice with a tiny twist (greater than versus less than), and could easily have been turned into a directive that would eliminate the need for maintaining the same code two times over.

    Come to think of it, I'm pretty sure that code should have been turned into a directive, but I'd have to read it in context to be sure.

    In the end I think you should go with the solution that makes you feel most comfortable and that you feel best equipped for supporting. At least when you make a choice of one over the other, you're making a kind of informed choice now :)

    I won't be able to follow this very actively for a little while, so I hope you'll find the solution that works best for you somewhere in all of this. Best of luck to you!

    -Kenn

  • Warren Buckley 2106 posts 4836 karma points MVP 7x admin c-trib
    Jul 28, 2014 @ 15:16
    Warren Buckley
    0

    Hello all,
    To add more to the confusion & choice of options my colleague Ale, at work come across serverValidationManager.
    So it is possible to give a better contextual error message than firing the notificationsService.

    This is the line I have added to give it a better contextual error message rather than this 'Property has errors'

    //Property Alias, Field name (ID/name of text box), Error Message
    serverValidationManager.addPropertyError($scope.model.alias, "username", "The user '" + username + "' could not be found on YouTube");
    

    Again I have stumbled across another approach with the help of my colleague, I really would love someone from the Umbraco core team to validate & go through the different options for validation, with when & where to use approach. For anyone following along with this thread, here is my updated funtion in my controller for the query of the username when the styled button is clicked.

    $scope.queryChannel = function(username) {
    
        //Debug info
        //console.log("Query Channel Click", username);
    
        //Default flag for validity
        var isThisValid = false;
    
        //Query this via our resource
        YouTubeResource.queryUsernameForChannel(username).then(function(response) {
    
            //Debug info
            //console.log("Value back from query API", response);
            //console.log("Items length", response.data.items.length);
    
    
            //Only do this is we have a result back from the API
            if(response.data.items.length > 0){
                //Data we are interested is in
                //response.data.items[0]
                var channel = response.data.items[0];
    
    
                //Create new JSON object as we don't need full object from Google's API response     
                var newChannelObject = {
                    "querriedUsername": username,
                    "channelId": channel.id,
                    "title": channel.snippet.title,
                    "description": channel.snippet.description,
                    "thumbnails": channel.snippet.thumbnails,
                    "statistics": channel.statistics
                };
    
                //Set the value to be our new JSON object
                $scope.model.value = newChannelObject;
    
                //Set our flag to true
                isThisValid = true;
            }
            else {
                //Fire a notification - saying user can not be found
                //notificationsService.error("YouTube User Lookup","The channel/user '" + username + "' could not be found on YouTube");
    
                //Set the value to be empty
                $scope.model.value = null;
    
                //Ensure flag is set to false
                isThisValid = false;
            }
    
    
            //Get the form with Umbraco's helper of this $scope
            //The form is wrapped just around this single prevalue editor
            var form = angularHelper.getCurrentForm($scope);
    
            //Inside the form we have our input field with the name/id of username
            //Set this field to be valid or invalid based on our flag
            form.username.$setValidity('YouTubeChannel', isThisValid);
    
            if(!isThisValid){
                //Property Alias, Field name (ID/name of text box), Error Message
                serverValidationManager.addPropertyError($scope.model.alias, "username", "The channel/user '" + username + "' could not be found on YouTube");    
            }
            else {
                //Property Alias, Field name (ID/name of text box)
                serverValidationManager.removePropertyError($scope.model.alias, "username");
            }
    
    
            //Debug
            //console.log("Form", form);
            //console.log("Form Username", form.username);
            //console.log("Is this Valid?", isThisValid);
    
        });     
    
Please Sign in or register to post replies

Write your reply to:

Draft