-1

I am currently rewriting the file tree in an AngularJS application. The way the file tree is implemented in the back consists of FileInfos, DirectoryInfos, and DirectoryEntrys. Essentially files and directories can be hooked in as many directories as you like because directories just contain entries pointing to a particular file or directory ID (even to a parent ID so you can go in a circle). I would argue this is not an ideal API, but what's done is done.

The reason I even bring up the backend API is that it is completely relevant to the frontend implementation. Basically, every Node has an ID pointing to the file or directory it corresponds to, but that alone cannot tell you where the node is in the hierarchy. Thus, when performing operations like adding/removing/renaming directory entries, you must know not only the entry name/ID but also the parent. To do this we could pass around arrays of nodes with both the parent and child (what we used to do), or keep a mapping of nodes to their parents, but both of these solutions are pretty messy. Ideally, the implementation in the frontend would be a doubly-linked graph where every directory node has a list of its children, and each of those children has a reference to the parent

I am well aware that you can call scope.$watch(expression, listener, true) to recursively watch all of an object's properties. However, this is never called for anything remotely related to our Node class. Yet with just the following recursive template, AngularJS begins to choke on cyclic object value errors.

<!-- Recursive node template -->
<script type="text/ng-template" id="tree-pane_node.html">
    <div style="padding-left: {{ level * 24 }}px" ng-click="$ctrl.navigateTo(node)">
        <a ng-click="$ctrl.toggleOpen(node); $event.stopPropagation()">
            <md-icon class="mdi mdi-24px" ng-class="node.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right'"></md-icon>
            <md-icon class="mdi mdi-24px mdi-{{ node.icon }}"></md-icon>
        </a>
        <span>{{ node.name }}</span>
    </div>
    <div ng-if="node.isOpen">
        <div ng-repeat="node in node.children | filter:{isDir: true} | orderBy:'name'" ng-init="level = level + 1">
            <div ng-include="'tree-pane_node.html'"></div>
        </div>
    </div>
</script>

<!-- Kick off the recursion with the top-level (special) directories -->
<div ng-repeat="node in ::$ctrl.topLevelDirectories" ng-init="level = 0" ng-include="'tree-pane_node.html'"></div>

Note: I omitted CSS classes as well as some expression callbacks like ng-right-click for brevity.

There are no template bindings to node.parent anywhere in the application. So why am I getting cyclic object value errors? Does AngularJS deep-watching template bindings by default? If so, can I turn this off? As I said earlier, I am currently rewriting the file tree portion of the code and I know this error appears as a result of storing parent references on nodes, not due to something else in the application.


I noticed the error originates from JSON.stringify()

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Sam Claus
  • 1,807
  • 23
  • 39
  • The AngularJS stringifies objects in templates that it watches, using a shallow watch of a string. The `ng-repeat` directive uses `$watchCollection` which does a shallow watch of each item in the collection. – georgeawg Oct 17 '19 at 19:23
  • @georgeawg I noticed the error originates from `JSON.stringify()`, but I couldn't find where it was being called in AngularJS. Do you mean to say all objects which are referenced in the template get serialized to JSON for dirty checking? If so, that seems like questionable framework design. I knew `ng-repeat` must shallow-watch the array's elements, but it shouldn't need to inspect each element deeply (and thus hit a cycle through `elem.parent`). – Sam Claus Oct 17 '19 at 19:42
  • The code in the question works as expected. Look elswhere to find the source of the problem. [1](https://plnkr.co/edit/hSBIhEpC5aulRU0OZFjV?p=preview). – georgeawg Oct 17 '19 at 21:19

1 Answers1

0

The AngularJS stringifies objects in templates that it watches, using a shallow watch of a string. The ng-repeat directive uses $watchCollection which does a shallow watch of each item in the collection.

I noticed the error originates from JSON.stringify()

All expressions in templates that are bound with double curly brackets ({{ }}) are stringified. I personnally would prefer to see content of objects instead "[object Object]".

To view objects in the template that have circular references, filter them to remove the circular references. One can use the JSON.decycle method implemented by Douglas Crockford. See his cycle.js. This allows you to stringify almost any standard structure.

For more information, see

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • I still think there is a miscommunication. I do not attempt to display an entire node in the template, i.e., `{{ node }}`. I bind to specific properties like in `{{ node.name }}` or `ng-repeat="child in node.children"`. – Sam Claus Oct 17 '19 at 20:17
  • Neither of those bindings would need to traverse the entire object, so why would Angular be stringifying the whole node? – Sam Claus Oct 17 '19 at 20:19
  • The code in the question works as expected. Look elswhere to find the source of the problem. [1](https://plnkr.co/edit/hSBIhEpC5aulRU0OZFjV?p=preview). – georgeawg Oct 17 '19 at 21:18
  • Your code literally calls `JSON.stringify()`. I know that `JSON.stringify()` is what throws the error. The question still remains: why is AngularJS even calling that? Thanks for trying, but if no one knows the answer off the top of their head I suppose I will just have to dig through the AngularJS source code. :/ – Sam Claus Oct 17 '19 at 23:28