Search code examples
javascriptangularjsellipsis

How to implement a (dynamic-width) text input with ellipsis and label in Angular?


I want to have a <input type="text"> in Angular app that displays an ellipsis, (if not under edit) if the value provided by the user is too long to be displayed in the UI.

The text input will be of dynamic width, having a text label next to it, and input should be taking all available space after the label.

The labels should be one-line regardless of length.

However, I know that HTML text inputs (<input>) can not have ellipsis, only regular HTML elements like <div> can. Can this be somehow achieved?


Solution

  • Have a look at this plunk

    Angular text input with ellipsis

    There are many things at play:

    • The value is displayed as a regular <span> (to be able to show ellipsis), and when clicked, the <input> is displayed instead (controlled by editing variable)
    • focusMe directive is used to give focus to the <input> when it's created to replace the <span>
    • nonBreaking filter to make the text label displayed in one line
    • various CSS rules based on flexbox control the display
    • overflow, text-overflow take care about ellipsis
    • white-space: nowrap and flex-shrink: 0 make sure that text label is not broken into multiple lines
    • flex-grow: 1 makes sure that <input> takes all extra space available

    Drawbacks:

    • when user clicks the static <span> for the first time, the cursor is put at the beginning of the <input>, not in the place where the user clicked (if he clicked in the middle)
    • when the labels are longer than viewport width, the textinput won't be displayed (assumption is that viewport width is always much wider than label length)

    Additional info:

    When you add validation, you probably want to keep the <input> displayed if the model is in invalid state, otherwise the bound value is empty and span will be empty.

    To do it:

    • pass "name" to the directive and inside do <input name="{{name}}">
    • change to <span ng-show="!editing && form.{{name}}.$valid">
    • change to <input ng-show="editing || !form.{{name}}.$valid">

    Full code:

    HTML:

    <!doctype html>
    <html ng-app="plunker">
    <head>
      <meta charset="utf-8">
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.js"></script>
      <script src="script.js"></script>
      <link rel="stylesheet" href="style.css">
      <script type="text/ng-template" id="InputTextWithEllipsis.html">
        <div class="input-text-with-ellipsis-wrapper">
          <label class="mylabel" ng-bind-html="mylabel | nonBreaking"></label>
          <span class="dual-input-wrapper">
            <span
              ng-show="!editing"
              ng-click="editing = true"
              class="static-text-with-ellipsis">{{mymodel}}</span>
            <input
              ng-show="editing"
              ng-focus="editing = true"
              ng-blur="editing = false"
              focus-me="editing"
              ng-model="mymodel"
              class="editable-textinput" />
          </span>
        </div>
      </script>
    </head>
    <body ng-controller="MainCtrl">
      <form name="profile">
        <input-text-with-ellipsis
          mylabel="'Name'"
          mymodel="dataModel.name"
        ></input-text-with-ellipsis>
    
        <input-text-with-ellipsis
          mylabel="'Last Name'"
          mymodel="dataModel.lastName"
        ></input-text-with-ellipsis>
    
        <input-text-with-ellipsis
          mylabel="'A very long label here'"
          mymodel="dataModel.lastName"
        ></input-text-with-ellipsis>
      </form>
    </body>
    </html>
    

    JS:

    var myModule = angular.module('plunker', []);
    
    myModule.filter('nonBreaking', function($sce) {
      return function(inputStr) {
        var outputStr = inputStr.replace(/\s/g, '&nbsp;');
        return $sce.trustAsHtml(outputStr);
      };
    });
    
    /*
     * http://stackoverflow.com/a/14837021/245966
     */
    myModule.directive('focusMe', function($timeout, $parse) {
      return {
        link: function(scope, element, attrs) {
          var model = $parse(attrs.focusMe);
          scope.$watch(model, function(value) {
            if (value === true) {
              $timeout(function() {
                element[0].focus();
              });
            }
          });
        }
      };
    });
    
    myModule.controller('MainCtrl', function($scope) {
      $scope.dataModel = {
        name: "Fernando",
        lastName: "Fernandez Sanchez de la Frontera"
      }
    });
    
    myModule.directive('inputTextWithEllipsis', function(){
      return {
        restrict: 'E',
        templateUrl: 'InputTextWithEllipsis.html',
        require: ['^form'],
        scope: {
          mylabel: '=',
          mymodel: '='
        },
        link: function(scope, element, attrs, ctrls) {
          scope.editing = false;
        }
      };
    });
    

    CSS:

    * {
      font: 16pt sans-serif;
      border: 0;
      padding: 0;
      margin: 0;
      outline: 0;
    }
    
    .input-text-with-ellipsis-wrapper {
      background-color: linen;
      padding: 10px;
    
      display: flex;
      flex-direction: row;
      align-items: flex-start;
      justify-content: space-between;
    }
    
    .mylabel {
      background-color: #ffddcc;
      margin-right: 10px;
    
      flex-basis: auto;
      flex-shrink: 0;
      min-width: 50px;
    }
    
    .dual-input-wrapper {
      flex-basis: auto;
      flex-grow: 1;
      overflow: hidden;
      white-space: nowrap;
      text-align: right;
    }
    
    .editable-textinput {
      background-color: #ddf;
    
      width: 100%;
      text-align: right;
    }
    
    .static-text-with-ellipsis {
      background-color: #eeccbb;
    
      display: block;
      overflow: hidden;
      text-overflow: ellipsis;
    }