'Unable to bind boolean to custom component

I've the following two Angular components with their respective templates:

Todo:

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.less'],
})
export class TodoComponent implements OnInit {
  @Input() uuid!: string;

  @Input() isCompleted!: boolean;
  @Input() title!: string;

  toggleCompletionStatus(): void {
    this.isCompleted = !this.isCompleted;
  }

  constructor() {}
  ngOnInit(): void {}
}
<mat-checkbox [checked]="isCompleted" (change)="toggleCompletionStatus()">
    <span class="todo-title">{{title}}</span>
</mat-checkbox>

and TodoContainer:

@Component({
  selector: 'app-todo-container',
  templateUrl: './todo-container.component.html',
  styleUrls: ['./todo-container.component.less'],
})
export class TodoContainerComponent implements OnInit {
  todos: Todo[] = [];

  constructor(private todoService: TodoService) {}

  ngOnInit(): void {
    this.todoService.getTodos().subscribe((todos) => {
      this.todos = todos;
    });
  }
}
<ol>
    <li *ngFor="let todo of todos">
        <div *ngIf="todo.isCompleted">
            <app-todo [id]="todo.id" [title]="todo.title" [isCompleted]="true"></app-todo>
        </div>
        <div *ngIf="!todo.isCompleted">
            <app-todo [id]="todo.id" [title]="todo.title" [isCompleted]="false"></app-todo>
        </div>
    </li>
</ol>

My problem is that I can't get bind to isCompleted from TodoContainer's template; the value is spelled out as either true or false, but tests reveal that it is always null. id and title bind properly. In fact, if I use isCompleted="true" instead of [isCompleted]="true", my tests pass (but the template has a type error of course). This has led me to believe that the issue is with isCompleted being a boolean.

How can I correctly bind a boolean value to an attribute?

EDIT: Below is the test on TodoContainer that's failing:

describe('TodoContainerComponent', () => {
  let component: TodoContainerComponent;
  let fixture: ComponentFixture<TodoContainerComponent>;
  let element: HTMLElement;
  let mockTodos: Todo[] = [];

  const todoService = jasmine.createSpyObj('TodoService', [
    'getTodos',
    'markCompletionStatus',
  ]);

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [TodoContainerComponent],
      providers: [{ provide: TodoService, useValue: todoService }],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(TodoContainerComponent);
    component = fixture.componentInstance;

    for (let i = 0; i < 10; ++i) {
      mockTodos.push({
        id: uuid4(),
        title: `title ${i}`,
        isCompleted: i % 2 == 0,
        ownerID: uuid4(),
      });
    }

    todoService.getTodos.and.returnValue(of(mockTodos));

    fixture.detectChanges();

    element = fixture.nativeElement;
  });

  it('should render todos', () => {
    const todoItems = element.getElementsByTagName('app-todo');

    expect(todoItems.length).toBe(mockTodos.length);
    for (let i = 0; i < todoItems.length; ++i) {
      expect(todoItems[i].getAttribute('id')).toBe(mockTodos[i].id);
    expect(todoItems[i].getAttribute('title')).toBe(mockTodos[i].title);

      // this doesn't pass:
      const isCompletedAttr = todoItems[i].getAttribute('isCompleted');
      expect(isCompletedAttr)
        .withContext(`completion status is ${isCompletedAttr}`)
        .not.toBeNull();
    }
  });
});


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source