0

At work I have to display a lot of tables with data that can change dynamically and quickly. To make this process easier I've been developing a declarative table directive.

Often there will be tables of stuff with Ids associated with them, like clients. I'm trying to place a link around the Id so that when the user clicks it, they will either get more detail or be directed to a different page. Neither the ng-click nor the ui-sref directives work when inserted into the DOM.

I have tried implementing the solution to this question: angular ng-bind-html and directive within it

////////// TABLE DIRECTIVE
const TABLE_TEMPLATE = `<table class="table table-striped">
  <thead>
    <tr ng-transclude></tr>
  </thead>

  <tbody>
    <tr ng-repeat="row in tbl.data">
      <td ng-repeat="col in tbl.columns"
        dc-compile="tbl.apply(col, row)"></td>
    </tr>
  </tbody>
</table>`;

class DcTableController {
  addColumn(col) {
    if (!this.columns) {
      this.columns = [];
    }

    this.columns.push(col);
  }

  apply(col, row) {
    let fields = col.field.split(",");
    let result = "";
    if (col.transform) {
      let $value = fields.length > 1 ?
        fields.map(field => row[field]) :
        row[col.field];

      return col.transform({ $value });
    } else {
      return fields.map(field => row[field]).join();
    }
  }
}

function dcTable() {
  return {
    restrict: "E",
    transclude: true,
    controller: DcTableController,
    controllerAs: "tbl",
    template: TABLE_TEMPLATE,
    scope: {},
    bindToController: {
      data: "="
    }
  };
}

////////// COLUMN DIRECTIVE
function dcColumn() {
  return {
    restrict: "E",
    replace: true,
    require: "^dcTable",
    transclude: true,
    template: "<th ng-transclude></th>",
    scope: {
      field: "@",
      transform: "&"
    },

    link(scope, elem, attrs, ctrl) {
      scope.transform = angular.isDefined(attrs.transform) ?
        scope.transform : false;
      ctrl.addColumn(scope)
    }
  };
}

////////// COMPILE DIRECTIVE
dcCompile.$inject = ["$compile"];
function dcCompile($compile) {
  return (scope, elem, attrs) => {
    scope.$watch(
      scope => scope.$eval(attrs.dcCompile),
      value => {
        elem.html(value);
        $compile(elem.contents())(scope);
      }
    );
  };
}

////////// MODULE CREATION
class DemoController {
  constructor() {
    this.employees = [
      {
        BadgeNo: "1701",
        FirstName: "Jean-Luc",
        LastName: "Picard",
        Salary: 0
      }
    ];
  }

  showAddress($event, badge) {
    console.log(badge);
    $event.preventDefault();
  }

  linkBadge(badge) {
    let a = document.createElement("a");
    a.href = "#";
    a.setAttribute("ng-click", `dc.showAddress($event, ${ badge })`);
    a.textContent = badge;
    return a.outerHTML;
  }

  formatName(names) {
    let [first, last] = names;
    return `${ last }, ${ first }`;
  }
}

I swear at one time it worked but for whatever reason I can't get it to work anymore. Here is the accompanying fiddle demonstrating the issue: https://jsfiddle.net/zachdunn/0n0m6dv7/1/

So when you do some logging to the console in the compile directive, you can see that the "contents" of the element being compiled includes the link with the ng-click attached to it, yet when you click on the link it does not call the function in the DemoController as it should.

In the title I mentioned that the compiling is in an ng-repeat loop. I don't really see why it would affect how the directive compiles but in the question I linked to, they weren't doing that and I haven't seen any other examples that implement the method inside of repeat clauses.

Community
  • 1
  • 1
dunnza
  • 478
  • 5
  • 17
  • 2
    Please [edit] your post to include the code (preferably as text). External links are no substitute for code in the post. – ryanyuyu Mar 17 '16 at 16:08

1 Answers1

0

It turns out my hunch about the repeat loops led somewhere. My problem is that the scope instance that I was compiling to did not have the dc property so it could not find the function to run when I clicked it.

To solve the problem I had to compile to scope.$parent.$parent.$parent. scope is bound to the innermost repeat loop (col in tbl.columns), the first $parent is bound to the outer repeat loop (row in tbl.data), the second $parent is bound to the table instance itself and the third $parent is the scope that contains the table and will thus provide access to the showAddress function.

I find it odd that angular did not throw an error when it couldn't find dc or showAddress, though I don't understand the inner workings enough to know how it determines what to call.

In any case, perhaps this will help someone in the future who forgets to account for parent/child scopes when dealing with repeat loops.

dunnza
  • 478
  • 5
  • 17