9

I am trying to create an input field that expands at least in width dynamically with the length of the string the user entered, probably even multiline. Is that possible with an input element in Angular Material 2?

With the textarea field from Angular Material 2 I only managed to expand the textarea in height, not in width with the following code:

<mat-form-field (keydown.enter)="onEnter()" 
                floatPlaceholder="never">
  <textarea matInput 
            placeholder="Autosize textarea" 
            matTextareaAutosize
            matAutosizeMinRows="1" 
            matAutosizeMaxRows="8">
  </textarea>
</mat-form-field>

also on StackBlitz.

In case of the textarea the scrollbar should be invisible or replaced by a smaller one. And most important pressing Enter should not create a new line but trigger an action only.

Night Train
  • 2,383
  • 3
  • 18
  • 35
  • 1
    Do you absolutely need to use an input? One solution I would propose is having an inline-block div that (through javascript) works like an input. that way it will expand as content increases. – Jack Dalton Mar 19 '18 at 10:38
  • that might do the trick, but I'm for sure not the first to try this (although i couldn't find this problem on stackoverflow). I just have the feeling there should be an easier option, like expanding the height of the `textarea`. – Night Train Mar 19 '18 at 10:43
  • 1
    I would suggest using "contenteditable" on a span is your best option in terms of avoiding errors, and insuring browser support. I cannot see what the advantage of using the input dom element would be. – Jack Dalton Mar 19 '18 at 10:48
  • well, I'll give that a try! But maybe it is possible to do that with Angular? – Night Train Mar 19 '18 at 10:52
  • I found a pretty awesome expanding input in this stackoverflow post https://stackoverflow.com/a/34224563/9058671 only downside: it's using jQuerry. here a jfiddle http://jsfiddle.net/kjxdr50a/ – Night Train Mar 22 '18 at 17:39

4 Answers4

7

simple and only template solution :

<mat-form-field
  [ngStyle]="{'width.ch': inputText.value.length, 'min-width.ch': 10}"> 
  <input  #inputText  matInput>
</mat-form-field>
N Fard
  • 1,063
  • 1
  • 15
  • 33
6

You can use ngStyle to bind width of the mat-form-field to a calculated value, and use the input event on the input to set that value. For example, here's an input who's width follows the text width over 64px:

<mat-form-field [ngStyle]="{'width.px': width}">
    <input #elasticInput matInput (input)="resize()">
</mat-form-field>
<span #hiddenText style="visibility: hidden; white-space: pre;">{{elasticInput.value}}</span>

export class InputTextWidthExample {

    @ViewChild('hiddenText') textEl: ElementRef;

    minWidth: number = 64;
    width: number = this.minWidth;

    resize() {
        setTimeout(() => this.width = Math.max(this.minWidth, this.textEl.nativeElement.offsetWidth));
    }
}

Obviously, this example uses a hidden span element for getting the text width, which is a little hacky. There is surely more than one way to calculate a string's width, including this.

Here is the example on Stackblitz.

G. Tranter
  • 16,766
  • 1
  • 48
  • 68
  • I changed `keyup` to `keydown`. That works a little smoother. The unit `ch` represents the width of the char `0`. https://developer.mozilla.org/en-US/docs/Web/CSS/length. Maybe there is no way around JS to measure the width... – Night Train Mar 19 '18 at 15:44
  • 1
    There are ways to measure text width, [here's one](https://blog.mastykarz.nl/measuring-the-length-of-a-string-in-pixels-using-javascript/). Since the keydown event hasn't updated the input's value string yet, you'll need to append the event character to the value string for measurement, and to do that properly you will need to differentiate between control characters like backspace and actual text as well as whitespace. So there are a number of little things to worry about that make this somewhat tedious, but it can work. – G. Tranter Mar 19 '18 at 17:18
  • that works awesome! in my case it might have been enough to set: `min-width: 30vw; width: 30vw; max-width: 98vw; text-align: center;` But that is much better!! – Night Train Mar 19 '18 at 19:55
1

I now created a more suitable solution for this problem. After I found a perfect solution in jQuery and @Obsidian added the corresponding JS code. I tried to adapt it for Angular input and came up with the following. I also added some scenarios that support cutting and pasting strings.

Here is a demo on StackBlitz and the corresponding code:

Template:

<style>
    #invisibleTextID {
      white-space: pre;
    }

    // prevents flickering:
    ::ng-deep .mat-form-field-flex {
      width: 102% !important;
    }
</style>

<mat-form-field
  #formFieldInput
  [ngStyle]="{'width.px': width}">

  <input
    #inputText
    id="inputTextID"
    matInput
    [(ngModel)]="inString"
    (paste)="resizeInput()"
    (cut)="resizeInput()"
    (input)="resizeInput()">

</mat-form-field>

<span #invisibleText id="invisibleTextID">{{ inString }}</span>

Resize method:

@ViewChild('invisibleText') invTextER: ElementRef;

inString: string = '';
width: number = 64;

resizeInput() {

    // without setTimeout the width gets updated to the previous length
    setTimeout ( () =>{

      const minWidth = 64;

      if (this.invTextER.nativeElement.offsetWidth > minWidth) {
        this.width = this.invTextER.nativeElement.offsetWidth + 2;
      } else {
        this.width = minWidth;
      }

    }, 0)
}
Night Train
  • 2,383
  • 3
  • 18
  • 35
0

I needed dynamic width for matInput inside a mat-table (dynamic size) and I experimented with several appraoches based on G.Tranter's solution. However, viewing a dynamic number of <span> elements depending on the rendered table cells was impractical. Therefore, I cam up with a rather practical solution for this:

Whenever the data is updated, I create an <span> element, fill it with the updated data, add it to the HTML body (or div in my case), extract the <span>'s offsetWidth and remove it immediately from the HTML document. While testing, I couldn't observe any flickering or rendering issue with this (though it is possible, I think).

Template

...

<!-- col1 Column -->
    <ng-container matColumnDef="col1">
      <th *matHeaderCellDef mat-header-cell mat-sort-header scope="col"> ifPattern</th>
      <td *matCellDef="let dataRecord;" mat-cell>

        <mat-form-field [ngStyle]="{'width.px': columnWidth[0]}" floatLabel="never">

          <input (input)="resize(0, dataRecord.col1)" [(ngModel)]="dataRecord.col1"
                 [value]="dataRecord.col1" matInput placeholder="col1 value">

        </mat-form-field>

      </td>
    </ng-container>
...

Resize Method

displayedColumns: string[] = ["col1", "col2", "col3"];
minWidth: number = 64; // pixel
columnWidth: number[] = [this.minWidth, this.minWidth, this.minWidth];

  resize(column_index: number, value: string) {
    setTimeout(() => {
      let span = document.createElement('span');
      span.innerText = value;
      let div = document.getElementById('case-cleaning-overview')
      div?.append(span);
      this.columnWidth[column_index] = Math.max(this.minWidth, span.offsetWidth);
      div?.removeChild(span);
    });
  }
Max M
  • 472
  • 4
  • 12