<!DOCTYPE html>
<html>

  <head>
    <link data-require="bootstrap-css@3.3.7" data-semver="3.3.7" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" />
    <script data-require="angular.js@1.6.2" data-semver="1.6.2" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.js"></script>
    <script data-require="ng-messages@*" data-semver="1.3.16" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular-messages.min.js"></script>
    <link rel="stylesheet" href="bootstrap-override.css" />
    <script src="script.js"></script>
    <script src="ra-text-input.component.js"></script>
    <script src="ra-app.component.js"></script>
    <script src="user.service.js"></script>
  </head>

  <body ng-app="app">
    <h2>Angular JS custom text input component</h2>
    <h3>Related to <a href="https://medium.com/front-end-hacking/angularjs-custom-text-input-component-4c1060b41118" target="_blank">AngularJS: custom text input component</a></h3>
    <ra-app></ra-app>
  </body>

</html>
angular.module("app", ["ngMessages"]);
/* Styles go here */

function InputController ($window, $document, $timeout) {
  var $ctrl = this;
  var sizes = {
    'sm': 'form-group-sm',
    'lg': 'form-group-lg',
  };
  
  $ctrl.$onInit = function () {
    $ctrl.elementId = $window.angular.copy($ctrl.name);
  }
  
  $ctrl.$onChanges = function (changes) {
    if (changes.required) {
      $ctrl.isRequired = $ctrl.required === "true";
    }
    if (changes.submitted) {
      $ctrl.isFormSubmitted = !!$ctrl.submitted;
    }
    if (changes.value) {
      $ctrl.internalValue = $window.angular.copy($ctrl.value);
    }
    if (changes.helpText) {
      $ctrl.hasHelpText = typeof $ctrl.helpText === 'string' &&
        $ctrl.helpText.length > 0; 
    }
    if (changes.size) {
      $ctrl.sizeClass = sizes[$ctrl.size];
    }
    if (changes.characterCounter) {
      $ctrl.hasCharacterCounter = $ctrl.characterCounter === "true";
    }
  }
  
  var domElement = undefined;
  $ctrl.$doCheck = function () {
    if (!$ctrl.hasCharacterCounter) {
      return;
    }
    if (!domElement) {
      if (!$ctrl.elementId) {
        return;
      }
      domElement = $document[0].querySelector('input[type=text]#' + $ctrl.elementId);
      if (!domElement) {
        return;
      }
    }
    var currentLength = domElement.value.length;
    if (currentLength !== $ctrl.currentLength) {
      $timeout(function _forceDigestToDisplayDataOnView () {
        $ctrl.currentLength = currentLength;
      });
    }
  };
  
  
  $ctrl.change = function () {
    $ctrl.onChange({
      name: $ctrl.name,
      value: $ctrl.internalValue,
    });
  }
  
  $ctrl.isNotValid = function () {
    return $ctrl.isFormSubmitted && (!$ctrl.form[$ctrl.name].$valid);
  }
  
  $ctrl.isValid = function () {
    return (!$ctrl.form[$ctrl.name].$pristine) && $ctrl.form[$ctrl.name].$valid;
  }
}

function raTextInputComponent () {
  return {
    templateUrl: "ra-text-input.component.html",
    controller: InputController,
    bindings: {
      name: "@",
      label: "@",
      required: "@",
      minLength: "@", 
      maxLength: "@",
      helpText: "@",
      size: "@",
      characterCounter: "@",
      value: "<",
      onChange: "&",
      form: "<",
      submitted: "<",
    }, 
  };
}

angular
  .module("app")
  .component("raTextInput", raTextInputComponent());
<div class="form-group has-feedback"
  ng-class="[$ctrl.sizeClass, {'has-error': $ctrl.isNotValid(), 'has-success': $ctrl.isValid()}]"
>
  <label class="control-label col-sm-3 col-xs-12" for="{{$ctrl.name}}">
    <span ng-bind="$ctrl.label"></span>
    <small ng-if="$ctrl.isRequired">
      <span aria-hidden="true" class="text-danger">
        *
      </span>
      <span class="sr-only">
        (field is required)
      </span>
    </small>
  </label>
  <div 
    class="col-sm-9 col-xs-12"
    id="{{$ctrl.name + '_input'}}"
  >
    <input type="text"
      class="form-control"
      id="{{$ctrl.name}}"
      name="{{$ctrl.name}}"
      ng-model="$ctrl.internalValue"
      ng-required="$ctrl.isRequired"
      ng-minlength="$ctrl.minLength"
      ng-maxlength="$ctrl.maxLength"
      ng-change="$ctrl.change()"
      aria-describedby="{{$ctrl.name + '_validation' + ' ' + $ctrl.name + '_validationErrors' + ($ctrl.hasHelpText? (' ' + $ctrl.name + '_validation') : '')}}"
    >
    <span
        class="glyphicon form-control-feedback" 
        ng-class="{'glyphicon-remove': $ctrl.isNotValid(), 'glyphicon-ok': $ctrl.isValid()}"
        aria-hidden="true"
    >
    </span>
    <small 
      id="{{$ctrl.name + '_validation'}}" 
      class="sr-only"
      aria-role="alert"
      aria-live="assertive"
    >
      <span ng-if="$ctrl.isValid()">
        Field has correct value
      </span>
      <span ng-if="$ctrl.isNotValid()">
        Field has incorrect value
      </span>  
    </small>
    <p
      class="help-block"
      ng-if="$ctrl.helpText" 
      ng-bind="$ctrl.helpText"
      id="{{$ctrl.name + '_helpText'}}"
    ></p>
    <p 
      class="help-block"
      ng-if="$ctrl.hasCharacterCounter"
    >
      <span
        ng-class="{'text-success': $ctrl.currentLength <= $ctrl.maxLength, 'text-danger' : $ctrl.currentLength > $ctrl.maxLength}"
      >
        Characters count: {{$ctrl.currentLength}} / {{$ctrl.maxLength}}
      </span>
    </p>
    <div
      class="text-danger"
      id="{{$ctrl.name + '_validationErrors'}}"
      aria-role="alert"
      aria-live="assertive"
    >
      <div ng-if="$ctrl.isFormSubmitted" ng-messages="$ctrl.form[$ctrl.name].$error">
        <div ng-message="required">
          Field is required
        </div>
        <div ng-message="minlength">
          Field should have at least {{$ctrl.minLength}} characters
        </div>
        <div ng-message="maxlength">
          Field should have maximum {{$ctrl.maxLength}} characters
        </div>
      </div>
    </div>
  </div>
</div>
<button type="button" class="btn btn-info" ng-click="$ctrl.load()">
  Load "from server" (faked with $q.when())
</button>
<form id="elementForm" name="elementForm" class="form-horizontal"   
  novalidate="" 
  ng-submit="elementForm.$valid && $ctrl.onSubmit()"
>
  <ra-text-input 
    name="firstName" 
    label="First name (large)" 
    min-length="2" 
    max-length="10" 
    value="$ctrl.element.firstName" 
    on-change="$ctrl.updateModel(name, value)" 
    form="elementForm" 
    required="true"
    submitted="elementForm.$submitted"
    size="lg"
    character-counter="true"
  ></ra-text-input>
  <ra-text-input 
    name="middleName" 
    label="Middle name"
    min-length="2" 
    max-length="10" 
    value="$ctrl.element.middleName" 
    on-change="$ctrl.updateModel(name, value)" form="elementForm" 
    required="false"
    submitted="elementForm.$submitted"
    help-text="Is not required, cause not everybody has middle name"
  ></ra-text-input>
  <ra-text-input 
    name="surname" 
    label="Surname (small)" 
    min-length="2" 
    max-length="15" 
    value="$ctrl.element.surname" 
    on-change="$ctrl.updateModel(name, value)" 
    form="elementForm" 
    required="true"
    submitted="elementForm.$submitted"
    size="sm"
  ></ra-text-input>
  <div class="form-group col-sm-offset-2">
    <div class="col-sm-offset-3 col-sm-9 col-xs-12">
      <button type="submit" class="btn btn-success">
        Submit!
      </button> 
    </div>
  </div>
</form>
function AppController (UserService) {
  var $ctrl = this;
  $ctrl.$onInit = function () {
    $ctrl.element = {};
  }
  
  $ctrl.updateModel = function (key, newValue) {
    $ctrl.element[key] = newValue;
  }
  
  $ctrl.onSubmit = function () {
    alert(JSON.stringify($ctrl.element));
  }
  
  $ctrl.load = function () {
    UserService.getElement().then(function bindElement (element) {
      $ctrl.element = element;
    });
  }
}

function raAppComponent () {
  return {
    templateUrl: "ra-app.component.html",
    controller: AppController,
    bindings: {
    }
  };
}

angular.module("app").component("raApp", raAppComponent());

/* http://stackoverflow.com/a/43664264/3368498 */
.form-horizontal .form-group-sm .control-label {
  font-size: 12px;
}

.form-horizontal .form-group-lg .control-label {
    font-size: 18px;
}
angular.module("app").factory("UserService", UserService);

function UserService ($q) {
  return {
    getElement: getElement,
  }
  
  function getElement () {
    return $q.when(
      {
        firstName: "John",
        middleName: "Maximilian",
        surname: "Doe",
      }
    );
  }
}