Applying web components to the project tasks app

(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:

Applying web components to the project tasks app

Start-up of app Project Tasks

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:

Applying web components to the project tasks app

All components of employee activated

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.

Add and remove Task propagations

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;
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.236.27