首页 > 解决方案 > get ElementRef.nativeElement width properties from component after data injection

问题描述

I have a component provided by an external Angular component library which is a private open-source project for the company I work for. The component has it's content injected into it from an Observable, and this data is not available at the initial render. The component in question is a Data Table Cell, part of a larger Data Table, much like the Material data table.

One of these cells has a long string that, depending on the data that is returned from the API, may overflow from the container. In this case, I need to shorten the string by chopping the middle out and replacing it with '...'. The string is a file path, e.g 'C:\really\really\really\really\really\long\path\to\file.txt'. Say the overflow was hidden from the '\path\to\file.txt' section, I would want the middle chopped out like so: 'C:\really\really...\long\path\to\file.txt'. This is similar to how macOS handles long file paths in Finder. I can do this fine, using a Pipe, however I'm unsure how to get the width of the component. I have tried using a ViewChild, like this:

@ViewChild('path') public path: ElementRef;

However, when I look at the measurement properties in the path.nativeElement object, they're all 0 - for example:

scrollHeight: 0
scrollLeft: 0
scrollTop: 0
scrollWidth: 0

I think this is because the data is injected into the table after the load. I have tried looking at the path variable in ngAfterViewInit, and it's getting the right component, but the innerHTML, text, content attributes are all empty. How would I get the most recent version of this variable?

Alternatively, if you have any other suggestions for how I could achieve this, that would be excellent. Thank you.

EDIT: Some updates. I have now got two options: I can use a Pipe in the HTML, and send the path ElementRef to the pipe, like this:

<div #path>{{ value | truncate: path }}</div>

And the Pipe code looks like this:

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {

  public transform(value: string, element: any): string {
    const { scrollWidth, clientWidth } = element;
    // computation to figure out what the value of the new string should be
    return value;
  }

}

The element has the un-truncated value in, but the scrollWidth and the clientWidth are equal, which is incorrect when the value is too long for the container (so it overflows); the scrollWidth should be longer than the clientWidth. This means I cannot compute how many characters should be truncated, as the pipe has no idea how far it's overflowing. I think the reason for these two variables being the same value is that the data has not yet been injected, so the element (a div, so a block element) is taking up the width of it's container. If I inspect the element after, I can see that scrollWidth is larger than the clientWidth.

The other way I've tried is to use a proxy function in my component class, which then returns the value from the pipe, like this:

public truncatePath(value: string, element: any) {
  return new TruncatePipe().transform(value, element);
}

and the HTML:

<div #path>{{ truncatePath(value, path) }}</div>

This works (clientWidth and scrollWidth are different), but it repeatedly runs the function, which is obviously hindering performance of the application.

EDIT 2: if I put the pipe code in a setTimeout, the clientWidth is the correct value and the string gets truncated, however as it's in the setTimeout it doesn't render that string.

标签: javascriptangulardom

解决方案


我使用管道找到了答案。这是我的代码:

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {

  public transform(value: string, element: any): Observable<string> {
    return new Observable<string>((observer: Observer<string>) => {
      setTimeout(() => {
        const { scrollWidth, clientWidth } = element;
        if (scrollWidth > clientWidth) {
          const avgCharWidth = Math.floor(scrollWidth / value.length);
          const middleOfString = Math.floor(value.length / 2);
          const numberToRemove = Math.ceil((scrollWidth - clientWidth) / avgCharWidth) + 1;
          const parts = [
            value.slice(0, middleOfString - (numberToRemove / 2)),
            '&#8230;',
            value.slice(middleOfString + (numberToRemove / 2))
          ];
          observer.next(parts.join(''));
        }
      });
      observer.next(value);
    });
  }

}

和 HTML:

<div #path [innerHTML]="value | truncate:path | async"></div>

一些注意事项:

  • 管道返回 之外的值setTimeout,否则该元素没有被渲染一次,因此没有 DOM 属性。然后它再次运行并检查初始值是否溢出,如果溢出,则计算中间带有“...”的新字符串。这确实意味着 Pipe 对每个值运行两次,这可能会因 100 行而变得相当昂贵,但我现在没有其他方法可以做到这一点。
  • AsyncPipe呈现来自 Observable 的最新值
  • 它需要在 setTimeout 内,以便它的渲染在单独的进程中

结果:

  • 原始字符串:C:\really\really\really\really\long\path\to\file1.txt(溢出容器)
  • 截断字符串:C:\really\really\real...long\path\to\file1.txt
  • 流程:管道运行一次,返回原值。然后 setTimeout 代码运行,发现原始字符串溢出并计算新字符串。异步管道更新渲染中的值。

--

  • 原始字符串:C:\path\to\file1.txt(适合容器)
  • 截断的字符串:C:\path\to\file1.txt
  • 流程:管道运行一次,返回原始值。setTimeout 代码运行,发现原始字符串 string 没有溢出,因此不会继续。

我很想知道是否有进一步的方法来改进这一点,以便 Pipe 不需要运行两次,并且删除 setTimeout 的方法也很棒。


推荐阅读