Henk-Jan Visscher Henk-Jan Visscher - 3 months ago 174
TypeScript Question

Angular 1.5 components : Component based application architecture

According to the Angular 1.5 documentation components should only control their own View and Data.

Instead of changing properties of objects passed to the component, a component should create an internal copy of the original data and use callbacks to inform the parent component when this copy has changed.

In this plunk I created a small demo illustrating my problem.

interface IStudent {
id: number,
name: string;
}

/* SERVICE: StudentService */

public class StudentsService {
static $inject = ['$q'];
constructor(private $q: ng.IQService) {
}

public getStudents() : ng.IPromise<IStudent[]> {
return this.$q.when([
{ id: 1, name: 'Adam' },
{ id: 2, name: 'Ben' }
}]);
}
}

/* COMPONENT: student */

class StudentsComponent implements ng.IComponent {
public template = `<student-list on-selected="$ctrl.onSelected(student)"></student-list>
<edit-student student="$ctrl.student" ng-show="$ctrl.student" on-changed="$ctrl.copyChanged(copy)"></edit-student>`;
public controller = StudentsController;
}

class StudentsController {
private student: IStudent;

protected onSelected(student: IStudent) {
this.student = student;
}

protected copyChanged(copy: IStudent) {
this.student.name = copy.name;
}
}

/* COMPONENT: student-list */

class StudentListComponent implements ng.IComponent {
public template = '<ul><li ng-repeat="student in $ctrl.students"><a ng-click="$ctrl.onClick(student)">{{ student.name }}</a></li></ul>';
public controller = StudentListController;
public bindings : any = {
onSelected: '&'
}
}

class StudentListController {
protected students: IStudent[];

static $inject = ['studentsService'];
constructor(private studentsService: StudentsService) {
}

public $onInit() {
this.studentsService.getStudents().then(data => this.students = data);
}

protected onClick(student: IStudent) {
this.onSelected({ student: student });
}
}

/* COMPONENT: edit-student */

class EditStudentComponent implements ng.IComponent {
public template = `<form class="form">
<div class="input-group">
<label for="#" class="control-label">Original</label>
<input type="text" class="form-control" ng-model="$ctrl.student.name" readonly>
</div>
</form>
<form class="form">
<div class="input-group">
<label for="#" class="control-label">Copy</label>
<input ng-change="$ctrl.changed()" type="text" class="form-control" ng-model="$ctrl.copy.name">
</div>
</form>`;
public controller = EditStudentController;
public bindings :any = {
student: '<',
onChanged: '&'
};
}

class EditStudentController {
protected copy: IStudent;

public $onInit() {
console.log('EditStudentComponent.$onInit', this.student);
}

public $onChange() {
console.log('EditStudentComponent.$onChange', this.student);
this.copy = angular.copy(this.student);
}

protected changed() {
console.log('EditStudentController.changed', this.copy);
this.onChanged({ copy: this.copy });
}
}

/* Bootstrap */

angular
.module('app', [])
.component('students', new StudentsComponent())
.component('studentList', new StudentListComponent())
.component('editStudent', new EditStudentComponent())
.service('studentsService', StudentsService)
;

angular.bootstrap(document, ['app']);


I have a list iterating over students. When the user selects a student, a textbox is shown in which the user can change the name of the student. Whenever the name changes, this change is propagated to the parent component which updates the list.

The problem is that after selecting a student in the list, the edit-user component is not initialized and still shows the name of the copy created when the component was created (null).

Can someone tell me how to fix this plunk such that, when clicking a student in the list, the edit component gets initialized with a copy of the selected student?

Edit: changed the plunk, as I accidentally removed the script tag instead of the style tag.

Answer

I thought this plunk represented my problem, but alas it didn't. The plunk didn't work because I implemented $onChange instead of $onChanges. I fixed the plunk such that it works as expected.

The cause of my original problem was a completely different one. In my business application I used another component with a ng-transclude directive around my edit component, like this:

<modal-editor>
    <edit-student data="$ctrl.data">
    <edit-student>
</modal-editor>

As the edit-student component was defined in the isolated scope of the modal-editor component, it didn't receive any changes made to the data variable in the outer scope (but somehow it could still access the data from this outer scope).

After modifying the modal-editor component such that it passed the data to the child component, everything worked as expected:

<modal-editor data="$ctrl.data">
    <edit-student data="$ctrl.data">
    <edit-student>
</modal-editor>