Reputation: 890
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.
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.
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);
}
}
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');
}
}
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.
Upvotes: 1
Views: 350
Reputation: 2929
There is nothing wrong with your bindings. It's the input type="date"
that messes things up. You can verify this by doing the following:
In task-web-api.ts, change getTaskDetails
to the following:
getTaskDetails(id) {
this.isRequesting = true;
return new Promise(resolve => {
setTimeout(() => {
let found = tasks.filter(x => x.id == id)[0];
let res = JSON.parse(JSON.stringify(found));
// Note these
console.log('Found: ', found);
console.log('Result: ', res);
resolve(res);
this.isRequesting = false;
}, latency);
});
}
And in task-detail.ts, change activate
to this:
activate(params, routeConfig) {
this.routeConfig = routeConfig;
return this.api.getTaskDetails(params.id).then(task => {
// Note this:
console.log('Task: ', task);
this.task = <iTask>task;
this.routeConfig.navModel.setTitle(this.task.title);
}).then(() => this.api.getTaskStatuses())
.then((statuses) => this.taskStatuses = statuses);
}
Then open a task for the first time:
Now navigate to another task:
You can see (in the console warning message) that the date is now in invalid format and therefore the control rejects this value. This rejection propagates into the binding, since it is a two-way one. This causes the value in your task object to be erased.
If, however, you change the input type to text
instead of date
, it will work:
<div class="form-group">
<label for="taskStartDate" class="control-label col-sm-2">Start Date</label>
<div class="col-sm-4">
<input type="text" 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="text" placeholder="Due Date" id="taskDueDate" class="form-control" value.bind="task.dueDate" />
</div>
</div>
In summary, it is not the binding that causes this behavior. Instead, it happens because the format is invalid and therefore the value is rejected. Since according to this answer, there is no standard way of altering what format an input type="date"
uses, you should use the standard yyyy-MM-dd
format. If you change your dummy data in task-web-api.ts, things start to work with input type="date"
as well.
At the same time, it is indeed strange that this does not happen after the first binding but it does later on. However, I do not believe it is Aurelia that causes this problem, it's probably the browser. Still, it might be worth creating an issue about it.
Upvotes: 1