I'm going through the Aurelia tutorials and, after finishing the ContactManager tutorial, I thought that I'd get some practice by modifying the Contact Manager site into a Task List site. I've changed the contact-list component and the contact-detail component to use tasks instead of contacts. Basically, when I click on a task in the task list, the router opens a task detail component; very similar to the way things work in the Contact Manager tutorial.
Question
In the task detail component I have some text in a <p>
element that is bound to some model values using string interpolation.
<p>Date: | ${task.startDate} | ${task.dueDate}</p>
When I click on a task in the task list for the first time, the task detail view opens up in the <router-view>
element and the interpolated string is correctly rendered, e.g. Dates: | 12/25/2017 | 1/25/2018
If I click on another task in the task list, all of the fields in the task detail view correctly change except for the text in the <p>
element. It becomes Dates: | |
To get the text in the <p>
element to appear, I have to clear out the selection by having the router put another view in and then select another task again. This re-renders the view and the interpolated value appears again.
Why do I have to re-render the task detail view in order for the values from the viewmodel to be bound to the view?
I'm going to post the code and markup below but I've also pushed some sample code into a public Github repo in case that helps. I tried putting it into a Gist.Run but since I'm using Typescript, that was proving to be problematic.
TaskList component
task-list.html:
<template>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
<tr repeat.for="task of tasks" class="testClass ${task.id === $parent.SelectedTask.id ? 'active' : ''}" click.delegate="selectTask(task)">
<td>${task.id}</td>
<td>${task.title}</td>
<td>${task.priority}</td>
</tr>
</tbody>
</table>
</template>
task-list.ts:
import {autoinject} from 'aurelia-framework';
import {TaskWebAPI} from './task-web-api';
import {Task} from './task-web-api';
import {Router} from 'aurelia-router';
//@inject(TaskWebAPI)
@autoinject
export class TaskList {
tasks;
SelectedTask: Task;
constructor(private api: TaskWebAPI, private router: Router) {
this.SelectedTask = new Task();
this.SelectedTask.id = 0;
}
created() {
this.api.getTaskList().then(tasks => this.tasks = tasks);
}
selectTask(task) {
this.router.navigateToRoute("tasks", { id: task.id });
this.SelectedTask = new Task();
Object.assign(this.SelectedTask, task);
}
}
TaskDetail Component
task-detail.html:
<template>
<form class="form-horizontal">
<div class="form-group">
<label for="taskTitle">Title</label>
<input type="text" placeholder="Task Title" id="taskTitle" class="form-control" value.bind="task.title" />
</div>
<div class="form-group">
<label for="taskPriority" class="control-label col-sm-2">Priority</label>
<div class="col-sm-3">
<input type="number" placeholder="Task Priority" id="taskPriority" class="form-control" value.bind="task.priority" />
</div>
<label for="taskStatus" class="control-label col-sm-2">Status</label>
<div class="col-sm-4">
<select class="form-control" value.bind="task.status">
<option repeat.for="status of taskStatuses">${status}</option>
</select>
</div>
</div>
<div class="form-group">
<label for="taskPctComplete" class="control-label col-sm-2">Percent Complete</label>
<div class="col-sm-3">
<input type="number" placeholder="Task % Complete" id="taskPctComplete" class="form-control" value.bind="task.percentComplete" />
</div>
</div>
<div class="form-group">
<label for="taskStartDate" class="control-label col-sm-2">Start Date</label>
<div class="col-sm-4">
<input type="date" placeholder="Start Date" id="taskStartDate" class="form-control" value.bind="task.startDate" />
</div>
<label for="taskDueDate" class="control-label col-sm-2">Due Date</label>
<div class="col-sm-4">
<input type="date" placeholder="Due Date" id="taskDueDate" class="form-control" value.bind="task.dueDate" />
</div>
</div>
<div class="form-group">
<label for="taskDescription" class="control-label col-sm-2">Description</label>
<div class="col-sm-10">
<textarea class="form-control" rows="4" id="taskDescription" value.bind="task.description"></textarea>
<!--This is the area place where the string interpolation is doing something that I don't understand. -->
<p>
Dates: | ${task.startDate} | ${task.dueDate}
</p>
</div>
</div>
<button type="button" class="btn btn-default" click.trigger="cancelClick()">Cancel</button>
</form>
</template>
task-detail.ts
import {autoinject} from 'aurelia-framework';
import {TaskWebAPI} from './task-web-api';
import {Router} from 'aurelia-router';
@autoinject
export class TaskDetail {
routeConfig;
task;
taskStatuses;
constructor(private api: TaskWebAPI, private router: Router) { }
activate(params, routeConfig) {
this.routeConfig = routeConfig;
return this.api.getTaskDetails(params.id).then(task => {
this.task = task;
this.routeConfig.navModel.setTitle(this.task.title);
}).then(() => this.api.getTaskStatuses())
.then((statuses) => this.taskStatuses = statuses);
//this.api.getTaskStatuses();
}
cancelClick() {
this.router.navigateToRoute('noselection');
}
}
Task-Web-Api
let latency = 200;
let id = 0;
function getId(){
return ++id;
}
export class Task {
id: number;
title: string;
priority: number;
status: string;
percentComplete: number;
description: string;
startDate: Date;
dueDate: Date;
}
let taskStatuses = ['Not Started', 'In Progress', 'Deferred', 'Completed'];
let tasks = [
{
id:getId(),
title:'TestTask1',
priority:'1',
status:'In Progress',
percentComplete:'22',
description:'This is the first test task.',
startDate:'12/25/2017',
dueDate:'1/25/2018'
},
{
id:getId(),
title:'TestTask2',
priority:'1',
status:'In Progress',
percentComplete:'45',
description:'This is the second test task.',
startDate:'1/25/2017',
dueDate:'11/25/2017'
},
{
id:getId(),
title:'TestTask3',
priority:'2',
status:'In Progress',
percentComplete:'89',
description:'This is the third test task.',
startDate:'4/25/2017',
dueDate:'9/25/2018'
},
{
id:getId(),
title:'TestTask4',
priority:'2',
status:'In Progress',
percentComplete:'10',
description:'This is the fourth test task.',
startDate:'5/25/2017',
dueDate:'7/16/2017'
},
{
id:getId(),
title:'TestTask5',
priority:'3',
status:'Not Started',
percentComplete:'0',
description:'This is the fifth test task.',
startDate:'',
dueDate:''
}
];
export class TaskWebAPI {
isRequesting = false;
getTaskList(){
this.isRequesting = true;
return new Promise(resolve => {
setTimeout(() => {
let results = tasks.map(x => { return {
id:x.id,
title:x.title,
priority:x.priority,
status:x.status,
percentComplete:x.percentComplete,
description:x.description,
startDate:x.startDate,
dueDate:x.dueDate
}});
resolve(results);
this.isRequesting = false;
}, latency);
});
}
getTaskStatuses() {
this.isRequesting = true;
return new Promise(resolve => {
setTimeout(() => {
let results = taskStatuses;
resolve(results);
this.isRequesting = false;
}, latency);
});
}
getTaskDetails(id){
this.isRequesting = true;
return new Promise(resolve => {
setTimeout(() => {
let found = tasks.filter(x => x.id == id)[0];
resolve(JSON.parse(JSON.stringify(found)));
this.isRequesting = false;
}, latency);
});
}
saveTask(task){
this.isRequesting = true;
return new Promise(resolve => {
setTimeout(() => {
let instance = JSON.parse(JSON.stringify(task));
let found = tasks.filter(x => x.id == task.id)[0];
if(found){
let index = tasks.indexOf(found);
tasks[index] = instance;
}else{
instance.id = getId();
tasks.push(instance);
}
this.isRequesting = false;
resolve(instance);
}, latency);
});
}
}
This is probably a stupid question, but why do I have to clear out the task-detail view every time in order for the interpolated string in the <p>
element to get bound? All of the other elements seem to get bound correctly just by switching to a new view. I know there's probably something that I don't understand about how this is all working and I'm looking for a nudge in the right direction.