2

I am currently trying to get a custom component working within Angular 2 written with ES6/ES7 and transpiled with Babel.

When I load up the page, I get the component accordion with all of the appropriate data present, but all of the panels are shut and clicking does nothing. I am modelling my accordion after this TypeScript Plunker. Additionally, the removeDynamic() function from AppComponent toggles when you click the button but the view does not update to show the change in data. I am at a loss after spending the better part of yesterday and today tinkering with it. Any insight would be much appreciated!

I have my AppComponent defined as such, with an appropriate template:

import {Component, View} from 'angular2/core'; // Import Component and View constructor (for metadata)
import {HTTP_PROVIDERS} from 'angular2/http'; // We're using http in our

import {Accordion, AccordionGroup} from '../components/accordion/accordion.component';

// Import NgFor directive
import {NgFor} from 'angular2/common';

// Annotate AppComponent class with `Component`
@Component({
  selector: 'my-app',

  // Let Angular know about `Http`
  providers: [HTTP_PROVIDERS]
})


// Define the view of our `Component` using one or more
// `View` annotations
@View({

  // Link to our external template file
  templateUrl: './components/app.html',

  // Specify which directives our `Component` will utilize with
  // the `directive` property of the `View` annotation
  directives: [Accordion, AccordionGroup, NgFor]
})

export class AppComponent {
  constructor() {

    // Debug
    console.log('AppComponent constructor() go');

    this.isOpen = false;

    this.groups = [
      {
        heading: 'Dynamic 1',
        content: 'I am dynamic!'
      },
      {
        heading: 'Dynamic 2',
        content: 'I am dynamic as well'
      }
    ];
  }

  removeDynamic() {
    this.groups.pop();

    // Debug
    console.log('removeDynamic() run');
  }
};

//export {AppComponent};

The template for AppComponent:

<p>
  <button type="button" class="btn btn-default" (click)="removeDynamic()">
    Remove last dynamic
  </button>
</p>

<accordion>
  <accordion-group heading="This is the header" is-open="true">
    This is the content
  </accordion-group>
  <accordion-group [heading]="group.heading" *ngFor="#group of groups">
    {{group.content}}
  </accordion-group>
  <accordion-group heading="Another group" [is-open]="isOpen">
    More content
  </accordion-group>
</accordion>

My Angular app is bootstrapped elsewhere due to Webpack.

I have a custom Accordion component written using Angular 2 with ES6/ES7:

// Import Inject, Component and View constructor (for metadata)
import {Inject} from 'angular2/core';
import {Component, View} from 'angular2/core';
// Import NgClass directive
import {NgClass} from 'angular2/common';

// # Accordion Component

// Annotate Accordion class with `Component`
@Component({
  // Define how we can use our `Component` annotation with
  // the `selector` property. 
  selector: 'accordion, [accordion]',

  // Modify the `host` element with a css class designator
  host: {
    'class': 'panel-group'
  }
})

// Define the view of our `Component` using one or more
// `View` annotations
@View({

  // Link to our external template file
  templateUrl: './components/accordion/accordion.html'
})

// Create and export `Component` class
export class Accordion {

  constructor() {
    // Debug
    console.log('Accordion constructor() go');

    this.groups = [];
  }

  // Function to register groups
  addGroup(group) {
    this.groups.push(group);
  }

  closeOthers(openGroup) {
    this.groups.forEach((group) => {
      if(group !== openGroup) {
        group.isOpen = false;
      }
    });
  }

  removeGroup(group) {
    let index = this.groups.indexOf(group);

    if(index !== -1) {
      this.groups.splice(index, 1);
    }
  }
}

// # AccordionGroup Component

// Annotate AccordionGroup class with `Component`
@Component({
  selector: 'accordion-group, [accordion-group]',

  // Specify (with the `inputs` property) that we are using a `heading`
  // attribute in our  component which will be mapped to a `heading`
  // variable in our `Component`.
  inputs: ['heading', 'isOpen'],

  // Let Angular know about any `providers`
  providers: []
})

// Define the view of our `Component` using one or more
// `View` annotations
@View({

  // Link to our external template file
  templateUrl: './components/accordion/accordion-group.html',

  // Specify which directives our `Component` will utilize with
  // the `directive` property of the `View` annotation
  directives: [NgClass, Accordion]
})

// Create and export `Component` class
export class AccordionGroup {

  constructor(accordion) {
    // Debug
    console.log('AccordionGroup constructor() go');

    this._isOpen = false;

    this.accordion = accordion;

    this.accordion.addGroup(this);
  }

  // Angular 2 DI desugar'd
  // Reference: https://stackoverflow.com/questions/33026015/how-to-inject-angular2-http-service-into-es6-7-class
  /*static get parameters() {
    return [[Accordion]];
  }*/

  toggleOpen(event) {
    event.preventDefault();
    this.isOpen = !this.isOpen;
    this.accordion.closeOthers(this);
  }

  onDestroy() {
    this.accordion.removeGroup(this);
  }
}

// The above desugar'd Angular 2 DI should work, but doesn't. This
// line seems to accomplish what we are looking for though.
AccordionGroup.parameters = [[Accordion]];

//export {Accordion, AccordionGroup};

This is the template for the Accordion component:

<ng-content></ng-content>

And the template for my AccordionGroup component:

<div class="panel panel-default" [ngClass]="{'panel-open': isOpen}">
  <div class="panel-heading" (click)="toggleOpen($event)">
    <h4 class="panel-title">
      <a href tabindex="0"><span>{{heading}}</span></a>
    </h4>
  </div>
  <div class="panel-collapse" [hidden]="!isOpen">
    <div class="panel-body">
      <ng-content></ng-content>
    </div>
  </div>
</div>

As an aside, a lot of the code came from this particular tutorial which demonstrates all of the Angular 2 stuff with TypeScript, so I simply adapted it to my es6/es7 environment with Webpack | Migrating Directives to Angular 2

Update: I have attempted both of the answers submitted and neither has solved the problem of the view not updating.

Here is a screen capture of how the component is currently behaving:

The proper data is present, yet the view isn't updating

And another of the debug logs showing data manipulation:

The appropriate data manipulation is present, but I am at a loss

I am at a loss here guys, and I really can't use TypeScript :(

datatype_void
  • 433
  • 5
  • 23

2 Answers2

3

Edited following the Mark's comment

In fact, it's the way Angular2 handles the change detection. "When change detection runs and it dirty checks an object, it does so by checking to see if the object reference changed (it does not check to see if the contents of the object changed)." (from Mark's comment)

(If you're interested in the way Zones are used in Angular2, you could have a look at the great answer from Mark: What is the Angular2 equivalent to an AngularJS $watch?).

So you need to refactor a bit your removeDynamic method:

export class AppComponent {
  (...)

  removeDynamic() {
    this.groups.pop();

    this.groups = this.groups.slice();

    // Debug
    console.log('removeDynamic() run');
  }
}

See this answer regarding the use of the slice method:

Here is the answer from the Angular team regarding this behavior: https://github.com/angular/angular/issues/6458.

Hope it helps you, Thierry

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • While this probably fixes the `removeDynamic` function, my content still doesn't when clicked, and the button still doesn't update the view for some reason. Using `console.log(this.groups)` with each calling of the function shows that the data is being removed from the array, even with my original function however the view just isn't updating for some reason. Regardless, I really appreciate the help. – datatype_void Jan 29 '16 at 15:35
  • 2
    "updates within objects don't trigger change detection" -- I would word this a bit differently. Change detection always runs after an event fires (the event triggers change detection, because of the Zone.js hooks). I would rephrase as follows: when change detection runs and it dirty checks an object, it does so by checking to see if the object reference changed (it does not check to see if the contents of the object changed). – Mark Rajcok Jan 29 '16 at 16:51
  • You're definitely right. My sentence was ambiguous! Thanks very much for pointing this out ;-) I updated my answer by quoting you... – Thierry Templier Jan 29 '16 at 16:59
  • Is it reasonable to assume that change detection isn't occurring then? Originally I thought it had something to do with DI but now it seems that everything is there, but no change detection is occurring when the data is manipulated? – datatype_void Jan 29 '16 at 17:04
  • When you only add an element to an array, corresponding view won't be updated... If you recreate the array, the view will. It's what I tried to explain in my answer. – Thierry Templier Jan 29 '16 at 17:06
  • So I am not sure what is causing these problems then, as the data is updating and I am using your method. I have double and triple checked everything. I am stumped – datatype_void Jan 29 '16 at 17:12
0

I got a barebones version working in this plnkr.

@Component({
  selector: 'my-app',
  providers: [],
  template: `
      <div class="panel panel-default" [ngClass]="{'panel-open': isOpen}">
        <div class="panel-heading" (click)="toggleOpen($event)">
          <h4 class="panel-title">
          </h4>
        </div>
        <div class="panel-collapse" [ngClass]="{'hidden': isOpen}">
          <div class="panel-body">
            <h3>geee</h3>
          </div>
        </div>
      </div>
  `,
  directives: [NgClass]
})
export class App {
  public isOpen = false;

  public groups = [
    {
      heading: 'Dynamic 1',
      content: 'I am dynamic!'
    },
    {
      heading: 'Dynamic 2',
      content: 'I am dynamic as well'
    }
  ];

  constructor(){
    this.isOpen = false;
    this.groups = [
      {
        heading: 'Dynamic 1',
        content: 'I am dynamic!'
      },
      {
        heading: 'Dynamic 2',
        content: 'I am dynamic as well'
      }
    ];
  }
  toggleOpen(event) {
    event.preventDefault();
    this.isOpen = !this.isOpen;
    //this.accordion.closeOthers(this);
  }

}

http://plnkr.co/edit/MjQ0GDimtqi20zmrDAEB?p=preview

inoabrian
  • 3,762
  • 1
  • 19
  • 27
  • If I change the `AccordionGroup` template to this, it doesn't render anything, seemingly. – datatype_void Jan 29 '16 at 04:57
  • No it renders the same and clicking still isn't toggling them as it should. – datatype_void Jan 29 '16 at 05:01
  • Unfortunately it looks like your example is with TypeScript and additionally, it removes all of the custom content transclusion which was sort of the point. I appreciate the help but this doesn't solve the problem at hand. – datatype_void Jan 29 '16 at 07:02
  • Here is a working plunker demonstrating with TypeScript the exact same functionality as what I am trying to recreate with es6: http://plnkr.co/edit/PvKuiBon0PpM6sNSehc6?p=preview. Yet I can't seem to recreate this with my efforts. I am starting to think maybe it has something to do with DI since I cannot use the `@Inject` decorator to inject `Accordion` into the `AccordionGroup` Component for use in the constructor. – datatype_void Jan 29 '16 at 07:04