(Get the code with: git clone git://github.com/dzenanr/polymer_project_tasks.git
)
The model that forms the basis for this app is a typical many-to-many relationship between the two entry concepts Project and Employee: a project has many employees, and an employee works on many projects. The many-to-many relationship between Project and Employee is normalized into two one-to-many relationships by introducing the intermediary Task concept, a Project consists of many Tasks and an Employee has many Tasks:
Project (1-n) Task and Employee (1-n) Task
A Project has a name (its ID), a description, and a Tasks collection. An Employee has an email
(its ID), a lastName
and a firstName
(both required), and a Tasks collection. A Task has a project, an employee, and a description: its ID is composed of the IDs of Project and Employee, so an employee can only have at most one task in a project. The code for this model is based on the Dartlero framework and can be found in the libmodel
folder. If you want to avoid redundancy, one relationship must be internal and the other one should be external; this is a subjective decision. Let's say that we will start more often with projects, find a project and display its tasks. In that case, the Project (1-n) Task relationship is internal. This means that you will have two hierarchical structures: projects with their tasks and employees only (employees without tasks). In each task, you will have a reference to its employee. The data could be saved as two JSON documents in two different files or in local storage under two different keys. You can see the difference in code by looking at the toJson
methods, for Project in the projects.dart
file:
Map<String, Object> toJson() { Map<String, Object> entityMap = new Map<String, Object>(); entityMap['code'] = code; entityMap['name'] = name; entityMap['description'] = description; entityMap['tasks'] = tasks.toJson(); // saving tasks return entityMap; }
Compared this with the same method for Employee in the employee.dart
file:
Map<String, Object> toJson() { Map<String, Object> entityMap = new Map<String, Object>(); entityMap['code'] = code; entityMap['lastName'] = lastName; entityMap['firstName'] = firstName; entityMap['email'] = email; return entityMap; }
(the same is true for the fromJson
methods).
When you load data, you need to recreate the Employee (1-n) Task relationship in both directions, by using the employee email
in each task. After the load, all relationships become internal and there are no reference IDs in the model (no employee
email
in every task, only employee
and project
properties), which means that the model in main memory is an object model. On start-up, our app instantiates the <polymer-app>
component in the polymer_project_tasks.html
file, which fires the PolymerApp.created()
constructor. Here, the model objects are created and the data is either loaded from local storage; or if data was not saved yet, it is initialized by calling the tasksModel.init()
method:
static const String EMPLOYEE_TASKS = 'polymer-employee-tasks'; static const String PROJECT_TASKS = 'polymer-project-tasks'; PolymerApp.created() : super.created() { tasksModel = TasksModel.one(); employees = tasksModel.employees; projects = tasksModel.projects; // tasksModel.init() // comment load to reinit load(); employees.internalList= toObservable(employees.internalList);(1) projects.internalList = toObservable(projects.internalList); (2) } load() { loadEmployees(); loadProjects(); } loadEmployees() { String json = window.localStorage[EMPLOYEE_TASKS]; if (json == null) { tasksModel.init(); } else { employees.fromJson(JSON.decode(json)); } employees.order(); } loadProjects() { String json = window.localStorage[PROJECT_TASKS]; if (json != null) { projects.fromJson(JSON.decode(json)); } projects.order(); }
Lines (1)
and (2)
are necessary so that the web components show employee or project updates when a new employee or project is added or removed: the List is observed by toObservable
. The web component defined in the polymer_app.html
file shows a Save button:
<button on-click="{{save}}">Save</button>
The save()
method is also contained in the polymer_app.dart
file and saves the data in local storage:
save(Event e, var detail, Node target) { saveEmployees(); saveProjects(); } saveEmployees() { window.localStorage[EMPLOYEE_TASKS] = JSON.encode(employees.toJson()); } saveProjects() { window.localStorage[PROJECT_TASKS] = JSON.encode(projects.toJson()); }
The initial screen shows all Projects and all Employees as shown in the following screenshot:
Here we use two web components: <project-table>
and <employee-table>
:
<project-table id="project-table" projects="{{ projects }}"> </project-table> <employee-table id="employee-table" employees="{{ employees }}"> </employee-table>
They are imported through:
<link rel="import" href="component/employee/employee_table.html"> <link rel="import" href="component/project/project_table.html">
As usual, the code of the components resides in webcomponent
.We see that both entities have an add functionality, together with edit, remove, and show tasks. The <employee-table>
component defined in the employee_table.html
file is indeed composed of three other components: <employee-add>
, <employee-edit>
, and <task-table>
, again shown in conditional templates:
<template if="{{showAdd}}"> <employee-add employees="{{employees}}"></employee-add> </template> <template if="{{showEdit}}"> <employee-edit employees="{{employees}}" employee="{{employee}}"></employee-edit> </template> <template if="{{showTasks}}"> <task-table id="task-table" employee="{{employee}}"> </task-table> </template>
Controlled by the Boolean variables showAdd
, showEdit
, and showTasks
; these are all marked as @observable
in the EmployeeTable
class.
The employees are shown through a repeating template in an HTML table:
<tbody template repeat="employee in employees.internalList"> <tr> <td>{{ employee.name }}</td> <td>{{ employee.email }}</td> <td><button on-click="{{edit}}" code="{{employee.code}}>(2) Edit</button></td> <td><button on-click="{{delete}}" code="{{employee.code}}> Remove</button></td> <td><button on-click="{{showEmployeeTasks}}" code="{{employee.code}}>Show</button></td> </tr> </tbody>
The behavior of the <employee-table> component is defined in the employee_table.dart file:
@CustomTag('employee-table') class EmployeeTable extends PolymerElement { @published Employees employees; Employee employee; @observable bool showAdd = false; @observable bool showEdit = false; @observable bool showTasks = false; show(Event e, var detail, Node target) { ButtonElement showAddButton = $['show-add']; (3) if (showAddButton.text == 'Show Add') { showAdd = true; showAddButton.text = 'Hide Add'; } else { showAdd = false; showAddButton.text = 'Show Add'; } } edit(Event e, var detail, Element target) { String code = target.attributes['code']; (4) employee = employees.find(code); showEdit = true; } delete(Event e, var detail, Element target) { String code = target.attributes['code']; employee = employees.find(code); for (var task in employee.tasks) { task.project.tasks.remove(task); } employees.remove(employee); showTasks = false; } showEmployeeTasks(Event e, var detail, Element target) { String code = target.attributes['code']; ButtonElement tasksButton = target; if (!showTasks && tasksButton.text == 'Show') { showTasks = true; employee = employees.find(code); employee.tasks.internalList = toObservable(employee.tasks.internalList); employee.tasks.order(); tasksButton.text = 'Hide'; } else if (showTasks && tasksButton.text == 'Hide') { showTasks = false; tasksButton.text = 'Show'; } } }
In line (3)
, we have used $['show-add']
as an alternative way of writing querySelector('#show-add')
. It will probably remind you of jQuery, and is included in Polymer. Note how the edit
event handler (as well as delete
and showEmployeeTasks
) gets passed the employee code through line (4)
, because it is an attribute of the button (see line (2)
). If we expand the three subcomponents of the <employee-table>
component, we get the following screen:
The <employee-add>
component is defined in the employee_add.html
file:
<polymer-element name="employee-add"> <template> <style> // left out </style> <br/> <label for="first-name">First Name</label> <input id="first-name" type="text" size="32"/> <br/> <label for="last-name">Last Name</label> <input id="last-name" type="text" size="32"/> <br/> <label for="email">Email</label> <input id="email" type="text" size="48"/> <br/> <button on-click="{{add}}">Add</button> <span id="message"></span> </template> <script type="application/dart" src="employee_add.dart"></script> </polymer-element>
The add
method in the employee_add.dart
file verifies that all fields are filled in. If so a new Employee
object is made and added (only when the employee was new) to the employees
collection:
class EmployeeAdd extends PolymerElement { @published Employees employees; add(Event e, var detail, Node target) { InputElement firstName = $['first-name']; InputElement lastName = $['last-name']; InputElement email = $['email']; Element message = $['message']; var error = false; message.text = ''; if (firstName.value.trim() == '') { message.text = 'employee first name is required; ${message.text}'; error = true; } if (lastName.value.trim() == '') { message.text = 'employee last name is required; ${message.text}'; error = true; } if (email.value.trim() == '') { message.text = 'employee email is required; ${message.text}'; error = true; } if (!error) { var employee = new Employee(); employee.firstName = firstName.value; employee.lastName = lastName.value; employee.email = email.value; if (employees.add(employee)) { message.text = 'added'; employees.order(); (1) } else { message.text = 'employee email already in use'; } } }
The <employee-table>
component shows the newly added employee because of line (1)
. The UI of the <employee-edit>
component is defined in the employee_edit.html
file; update is called in the employee_edit.dart
file:
class EmployeeEdit extends PolymerElement { @published Employees employees; @published Employee employee; @published String email; EmployeeEdit.created() : super.created(); enteredView() { super.enteredView(); email = employee.email; } update(Event e, var detail, Node target) { employee.email = email; employees.order(); // to see a new email in the list var polymerApp = querySelector('#polymer-app'), var employeeTable = polymerApp.shadowRoot.querySelector('#employee- table'), employeeTable.showEdit = false; } }
Deleting an employee is covered in the delete
method of the employee_table.dart
file. The Show Tasks button activates the <task-table>
component. Its <template>
definition in the task_table.html
file repeats over all the tasks:
<tbody template repeat="{{task in tasks.internalList}}"> <tr> <td>{{task.project.name}}</td> <td>{{task.employee.name}}</td> <td>{{task.description}}</td> <td><button on-click="{{edit}}" code="{{task.code}}">Edit</button></td> <td><button on-click="{{delete}}" code="{{task.code}}">Remove</button></td> </tr> </tbody>
The browser now shows the name of the employee or project. This component also envelops two other components: tasks can be added through the <task-add>
component, edited through the <task-edit>
component, and can also be removed.
A new task must be related to one project and one employee. This means that a new task, as one and only one object, must be added to two different collections of tasks, one for the project and the other for the employee. In this application, the internal Project-Task relationship is used to add a task to its project. The external Task-Employee relationship is used to lookup an employee for the new task of the project. In this application, a task cannot be added to an employee.
When a task is removed from project's tasks, it must also be removed from the employee's tasks, and vice versa. When a project is removed, its tasks cannot be accessed anymore. Those tasks must be removed from collections of tasks related to employees found in the project's tasks. Similarly, when an employee is removed, his/her tasks cannot be accessed anymore. Those tasks must be removed from collections of tasks related to projects found in the employee's tasks.
The add and remove propagations of tasks in the model must be reflected in the display of web components. When a task is updated (description only in this application), there is no need for update propagations in the model because there is only one task with the same project and the same employee. However, the same task (with the same project and the same employee) may be displayed in two different web components. When this task is updated, the new description must show up in the display of both web components.
Because it is built in exactly the same way, you should now be able to understand the other web components: <project-table>
, and its subcomponents <project-add>
and <project-edit>
, together with <task-table>
. Used in the Project context, the task
table also shows a <task-add>
component and a remove functionality. Now, here is the lookup of an employee when adding a task in the task_add.html
file:
<select id="employee-lookup"> <option template repeat="{{employee in employees.internalList}}"> {{employee.code}} </option> </select>
In the add
event handler, its value is retrieved with the following code:
SelectElement employeeLookup = $['employee-lookup']; String code = employeeLookup.value;
3.149.236.27