├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── angular.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── karma.conf.js ├── logo-clear-dark.png ├── logo-clear-dark.svg ├── logo-clear.png ├── logo-clear.svg ├── logo-ideas.ai ├── logo-ideas.jpg ├── logo-ideas.svg ├── package-lock.json ├── package.json ├── pluralsight-colours.png ├── protractor.conf.js ├── src ├── app │ ├── alltimes │ │ ├── alltimes.component.css │ │ ├── alltimes.component.html │ │ └── alltimes.component.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── dashboard │ │ ├── dashboard.component.css │ │ ├── dashboard.component.html │ │ └── dashboard.component.ts │ ├── fielderrors │ │ ├── fielderrors.component.css │ │ ├── fielderrors.component.html │ │ └── fielderrors.component.ts │ ├── profile │ │ ├── profile.component.css │ │ ├── profile.component.html │ │ ├── profile.component.spec.ts │ │ └── profile.component.ts │ ├── projects │ │ ├── projects.component.css │ │ ├── projects.component.html │ │ └── projects.component.ts │ ├── rxjs-operators.ts │ ├── settings │ │ ├── settings.component.css │ │ ├── settings.component.html │ │ └── settings.component.ts │ ├── statistic │ │ ├── statistic.component.css │ │ ├── statistic.component.html │ │ └── statistic.component.ts │ └── timesheet │ │ ├── sample.people.data.ts │ │ ├── sample.projects.data.ts │ │ ├── timesheet.component.css │ │ ├── timesheet.component.html │ │ └── timesheet.component.ts ├── assets │ ├── .gitkeep │ ├── data │ │ ├── hoursByTeam.json │ │ ├── people.json │ │ ├── projects.json │ │ └── teams.json │ └── img │ │ └── logo-clear.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts └── tsconfig.json ├── tslint.json └── xample-scripts ├── 1.intro.md ├── 2.charts.md ├── 3.forms.md ├── 4.grids.md ├── 5.tabs.md └── 6.advanced.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Glen Smith 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AgileTimes 2 | 3 | Welcome to the Sample app for my Pluralsight Course [Building Beautiful Angular Apps with PrimeNG](https://app.pluralsight.com/courses/angular-apps-prime-ng). 4 | 5 | The course was officially released on Thursday, Aug 24, 2017. 6 | 7 | I'll keep this page updated with breaking changes as the course ages. 8 | 9 | ## Take it for a spin 10 | 11 | I deploy the latest version of the application directly to github pages. 12 | 13 | [Try it out now](https://glenasmith.github.io/pluralsight-primeng/) 14 | 15 | 16 | ## Starter Kit 17 | 18 | Keen to start from scratch? You can (well, almost). There's a [starter-kit](https://github.com/glenasmith/pluralsight-primeng/tree/starter-kit) branch which you can clone/download which contains: 19 | * A basic menu template routing to blank components 20 | * Styling for the shell, but none of the component CSS - which you insert as you go 21 | * Blank component code shells for you to insert code as you follow along 22 | 23 | Remember: we don't cover every bit of CSS styling you need but I do refer when you need to copy and paste. So you might need to switch branches every now and again to grab some CSS styling. 24 | 25 | Go for it! 26 | 27 | 28 | ## Errata & Updates 29 | 30 | - 2019-03-12 - Updated to PrimeNG 7. Rebuilt starter kit branch to match new build. 31 | - 2018-11-15 - Major Update! Updated to PrimeNG 6.1.6 and Angular 7.0 using [update.angular.io](https://update.angular.io) 32 | - 2017-12-05 - Updated to PrimeNG 5.0.2 and Angular 5.0.5. And some layout fixes for new version. 33 | - 2017-10-26 - Changed base layout to absolute positioning for better resize screen experience. 34 | - 2017-09-17 - Introduced "starter-kit" branch for initial project template 35 | - 2017-08-28 - Fixed some CSS Dialog footer layout issues (my old 2.x CSS causing 4.x issues) 36 | - 2017-08-24 - Course officially released [here](https://app.pluralsight.com/courses/angular-apps-prime-ng) 37 | - 2017-08-24 - Bumped Deps to latest version (PrimeNG 4.1.3, Angular 4.3.6) 38 | 39 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "agile-times": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "node_modules/roboto-fontface/css/roboto/roboto-fontface.css", 25 | "node_modules/font-awesome/css/font-awesome.css", 26 | "node_modules/primeng/resources/primeng.css", 27 | "node_modules/primeng/resources/themes/nova-light/theme.css", 28 | "node_modules/quill/dist/quill.core.css", 29 | "node_modules/quill/dist/quill.snow.css", 30 | "node_modules/fullcalendar/dist/fullcalendar.css", 31 | "src/styles.css" 32 | ], 33 | "scripts": [ 34 | "node_modules/chart.js/dist/Chart.js", 35 | "node_modules/jquery/dist/jquery.js", 36 | "node_modules/quill/dist/quill.js", 37 | "node_modules/moment/min/moment.min.js", 38 | "node_modules/fullcalendar/dist/fullcalendar.js" 39 | ] 40 | }, 41 | "configurations": { 42 | "production": { 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "aot": true, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ] 58 | } 59 | } 60 | }, 61 | "serve": { 62 | "builder": "@angular-devkit/build-angular:dev-server", 63 | "options": { 64 | "browserTarget": "agile-times:build" 65 | }, 66 | "configurations": { 67 | "production": { 68 | "browserTarget": "agile-times:build:production" 69 | } 70 | } 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n", 74 | "options": { 75 | "browserTarget": "agile-times:build" 76 | } 77 | }, 78 | "test": { 79 | "builder": "@angular-devkit/build-angular:karma", 80 | "options": { 81 | "main": "src/test.ts", 82 | "karmaConfig": "./karma.conf.js", 83 | "polyfills": "src/polyfills.ts", 84 | "scripts": [ 85 | "node_modules/chart.js/dist/Chart.js", 86 | "node_modules/jquery/dist/jquery.js", 87 | "node_modules/quill/dist/quill.js", 88 | "node_modules/moment/min/moment.min.js", 89 | "node_modules/fullcalendar/dist/fullcalendar.js" 90 | ], 91 | "styles": [ 92 | "node_modules/roboto-fontface/css/roboto/roboto-fontface.css", 93 | "node_modules/font-awesome/css/font-awesome.css", 94 | "node_modules/primeng/resources/primeng.css", 95 | "node_modules/primeng/resources/themes/bootstrap/theme.css", 96 | "node_modules/quill/dist/quill.core.css", 97 | "node_modules/quill/dist/quill.snow.css", 98 | "node_modules/fullcalendar/dist/fullcalendar.css", 99 | "src/styles.css" 100 | ], 101 | "assets": [ 102 | "src/assets", 103 | "src/favicon.ico" 104 | ] 105 | } 106 | }, 107 | "lint": { 108 | "builder": "@angular-devkit/build-angular:tslint", 109 | "options": { 110 | "tsConfig": [ 111 | "src/tsconfig.json" 112 | ], 113 | "exclude": [] 114 | } 115 | } 116 | } 117 | }, 118 | "agile-times-e2e": { 119 | "root": "e2e", 120 | "sourceRoot": "e2e", 121 | "projectType": "application", 122 | "architect": { 123 | "e2e": { 124 | "builder": "@angular-devkit/build-angular:protractor", 125 | "options": { 126 | "protractorConfig": "./protractor.conf.js", 127 | "devServerTarget": "agile-times:serve" 128 | } 129 | }, 130 | "lint": { 131 | "builder": "@angular-devkit/build-angular:tslint", 132 | "options": { 133 | "tsConfig": [ 134 | "e2e/tsconfig.json" 135 | ], 136 | "exclude": [] 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | "defaultProject": "agile-times", 143 | "schematics": { 144 | "@schematics/angular:component": { 145 | "prefix": "at", 146 | "styleext": "css" 147 | }, 148 | "@schematics/angular:directive": { 149 | "prefix": "at" 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AgileTimesPage } from './app.po'; 2 | 3 | describe('agile-times App', function() { 4 | let page: AgileTimesPage; 5 | 6 | beforeEach(() => { 7 | page = new AgileTimesPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class AgileTimesPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "es2016" 9 | ], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "outDir": "../dist/out-tsc-e2e", 13 | "sourceMap": true, 14 | "target": "es6", 15 | "typeRoots": [ 16 | "../node_modules/@types" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-coverage-istanbul-reporter'), 12 | require('@angular-devkit/build-angular/plugins/karma') 13 | ], 14 | files: [ 15 | 16 | ], 17 | preprocessors: { 18 | 19 | }, 20 | mime: { 21 | 'text/x-typescript': ['ts','tsx'] 22 | }, 23 | coverageIstanbulReporter: { 24 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 25 | fixWebpackSourcePaths: true 26 | }, 27 | 28 | reporters: config.angularCli && config.angularCli.codeCoverage 29 | ? ['progress', 'coverage-istanbul'] 30 | : ['progress'], 31 | port: 9876, 32 | colors: true, 33 | logLevel: config.LOG_INFO, 34 | autoWatch: true, 35 | browsers: ['Chrome'], 36 | singleRun: false 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /logo-clear-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/logo-clear-dark.png -------------------------------------------------------------------------------- /logo-clear-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Agile Times 27 | 28 | 29 | -------------------------------------------------------------------------------- /logo-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/logo-clear.png -------------------------------------------------------------------------------- /logo-clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Agile Times 53 | 54 | 55 | -------------------------------------------------------------------------------- /logo-ideas.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/logo-ideas.ai -------------------------------------------------------------------------------- /logo-ideas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/logo-ideas.jpg -------------------------------------------------------------------------------- /logo-ideas.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xmlAgile Times 76 | Agile Times 111 | Agile Times 146 | Agile Times 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agile-times", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "404": "shx cp dist/index.html dist/404.html", 8 | "ng": "ng", 9 | "start": "ng serve", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "dist": "ng build --prod --base-href https://glenasmith.github.io/pluralsight-primeng/", 14 | "deploy": "angular-cli-ghpages --repo=git@github.com:glenasmith/pluralsight-primeng.git", 15 | "shiptogithub": "npm run dist && npm run 404 && npm run deploy" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "7.2.8", 20 | "@angular/common": "7.2.8", 21 | "@angular/compiler": "7.2.8", 22 | "@angular/core": "7.2.8", 23 | "@angular/forms": "7.2.8", 24 | "@angular/http": "7.2.8", 25 | "@angular/platform-browser": "7.2.8", 26 | "@angular/platform-browser-dynamic": "7.2.8", 27 | "@angular/router": "7.2.8", 28 | "@angular/cdk": "7.3.4", 29 | "chart.js": "2.7.3", 30 | "core-js": "2.5.1", 31 | "dexie": "2.0.4", 32 | "font-awesome": "^4.7.0", 33 | "fullcalendar": "4.0.0-alpha.2", 34 | "jquery": "3.3.1", 35 | "moment": "2.22.2", 36 | "primeicons": "1.0.0", 37 | "primeng": "7.0.5", 38 | "quill": "1.3.6", 39 | "roboto-fontface": "0.10.0", 40 | "rxjs": "6.4.0", 41 | "ts-helpers": "^1.1.1", 42 | "zone.js": "0.8.29" 43 | }, 44 | "devDependencies": { 45 | "@angular-devkit/build-angular": "0.13.5", 46 | "@angular/cli": "^7.3.5", 47 | "@angular/compiler-cli": "7.2.8", 48 | "@types/dexie": "1.3.1", 49 | "@types/jasmine": "2.8.2", 50 | "@types/node": "7.0.18", 51 | "codelyzer": "4.5.0", 52 | "jasmine-core": "2.8.0", 53 | "jasmine-spec-reporter": "4.2.1", 54 | "karma": "1.7.1", 55 | "karma-chrome-launcher": "2.2.0", 56 | "karma-cli": "^1.0.1", 57 | "karma-coverage-istanbul-reporter": "1.3.0", 58 | "karma-jasmine": "1.1.1", 59 | "protractor": "5.2.0", 60 | "shx": "0.3.2", 61 | "ts-node": "3.3.0", 62 | "tslint": "5.8.0", 63 | "typescript": "3.1.6" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pluralsight-colours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/pluralsight-colours.png -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/alltimes/alltimes.component.css: -------------------------------------------------------------------------------- 1 | .alltimesheets { 2 | background-color: white; 3 | font-family: "Roboto"; 4 | } 5 | 6 | .header { 7 | padding: 1em; 8 | color: white; 9 | background-color: #0275D8; 10 | margin-bottom: 1em; 11 | } 12 | 13 | h2 { 14 | font-weight: bolder; 15 | font-size: xx-large; 16 | display: inline; 17 | } 18 | 19 | h3 { 20 | font-weight: lighter; 21 | font-size: xx-large; 22 | display: inline; 23 | } 24 | 25 | p-dataTable >>> .ui-datatable-footer { 26 | min-height: 60px; 27 | } 28 | 29 | p-dataTable >>> .ui-datatable-footer input { 30 | margin-left: 0.5em; 31 | font-size: larger; 32 | padding: 3px; 33 | } 34 | 35 | p-dataTable >>> .ui-datatable-footer { 36 | min-height: 60px; 37 | } 38 | 39 | p-dataTable >>> .selectBoxColumn { 40 | width: 43px; 41 | } 42 | 43 | p-contextMenu >>> .ui-menuitem-active a { 44 | background-color: #F15B2A !important; 45 | } 46 | -------------------------------------------------------------------------------- /src/app/alltimes/alltimes.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |

7 | All Timesheets 8 |

9 |

10 | Click to edit Users and Projects 11 |

12 | 13 |
14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | -------------------------------------------------------------------------------- /src/app/alltimes/alltimes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { MenuItem, DataTable, LazyLoadEvent } from "primeng/primeng"; 3 | import { range } from 'rxjs'; 4 | import Dexie from 'dexie'; 5 | 6 | const MAX_EXAMPLE_RECORDS = 1000; 7 | 8 | @Component({ 9 | selector: 'at-alltimes', 10 | templateUrl: './alltimes.component.html', 11 | styleUrls: ['./alltimes.component.css'] 12 | }) 13 | export class AlltimesComponent implements OnInit { 14 | 15 | @ViewChild("dt") dt : DataTable; 16 | 17 | db: Dexie; 18 | 19 | allTimesheetData = [ 20 | 21 | { user: 'Glen', project: 'Payroll App', category: 'Backend', startTime: 1000, endTime: 1700, date: 1434243 }, 22 | { user: 'Karen', project: 'Agile Times', category: 'Frontend', startTime: 900, endTime: 1700, date: 1434243 }, 23 | { user: 'Si', project: 'Mobile App', category: 'Operations', startTime: 1100, endTime: 1700, date: 1434243 }, 24 | { user: 'Rohit', project: 'Agile Times', category: 'Backend', startTime: 800, endTime: 1700, date: 1434243 }, 25 | 26 | ]; 27 | 28 | allProjectNames = ['', 'Payroll App', 'Mobile App', 'Agile Times']; 29 | 30 | allProjects = this.allProjectNames.map((proj) => { 31 | return { label: proj, value: proj } 32 | }); 33 | 34 | selectedRows: Array; 35 | 36 | contextMenu: MenuItem[]; 37 | 38 | recordCount : number; 39 | 40 | constructor() { 41 | // for (let x = 0; x < 5; x++) { 42 | // this.allTimesheetData = this.allTimesheetData.concat(this.allTimesheetData); 43 | // } 44 | this.recordCount = this.allTimesheetData.length; 45 | 46 | this.configureDatabase(); 47 | this.populateDatabase(); 48 | 49 | } 50 | 51 | private configureDatabase() { 52 | 53 | this.db = new Dexie('AgileTimes'); 54 | 55 | // Define a schema 56 | this.db.version(1).stores({ 57 | timesheet: 'id,user,project,category,startTime,endTime,date' 58 | }); 59 | 60 | } 61 | 62 | private populateDatabase() { 63 | 64 | this.getRecordCount().then((count) => { 65 | this.recordCount = count; 66 | if (!count) { 67 | this.resetDatabase(); 68 | } 69 | }); 70 | 71 | } 72 | 73 | generateRandomUser(id: number) { 74 | 75 | var names = ["Joe", "Mary", "Phil", "Karen", "Si", "Tim", "Rohit", "Jenny", "Kim", "Greg", "Danni"] 76 | var allProjectNames = ['Payroll App', 'Mobile App', 'Agile Times']; 77 | var allCategories = ['Frontend', 'Backend', 'Operations']; 78 | 79 | let newUser = { 80 | id: id, 81 | user: names[id % names.length], 82 | project: allProjectNames[id % allProjectNames.length], 83 | category: allCategories[id % allCategories.length], 84 | startTime: Math.round(Math.random() * 1000), 85 | endTime: Math.round(Math.random() * 1000), 86 | date: Math.round(Math.random() * 100000) 87 | }; 88 | newUser.endTime += newUser.startTime; // to make sure it's later 89 | 90 | return newUser; 91 | 92 | } 93 | 94 | getRecordCount(): Dexie.Promise { 95 | return this.db.table("timesheet").count(); 96 | } 97 | 98 | resetDatabase() { 99 | 100 | let that = this; 101 | 102 | this.dt.loading = true; 103 | 104 | this.db.table("timesheet").clear().then(() => { 105 | console.log("Database Cleared"); 106 | range(0, MAX_EXAMPLE_RECORDS).subscribe( 107 | function (id) { 108 | let randomUser = that.generateRandomUser(id); 109 | that.db.table("timesheet").add(randomUser); 110 | if (id % 100 == 0) { 111 | that.getRecordCount().then((count) => { 112 | that.recordCount = count; 113 | }) 114 | } 115 | 116 | }, 117 | function (err) { 118 | console.log("Do Error: %s", err); 119 | }, 120 | function () { 121 | console.log("Do complete"); 122 | that.dt.loading = false; 123 | that.dt.reset(); 124 | console.log("Finished Reset database"); 125 | that.getRecordCount().then((count) => { 126 | that.recordCount = count; 127 | }) 128 | }); 129 | }) 130 | } 131 | 132 | loadTimes(event: LazyLoadEvent) { 133 | 134 | console.log(JSON.stringify(event)); 135 | 136 | let table = this.db.table("timesheet"); 137 | 138 | var query: any; 139 | 140 | // Dexie doesn't support ordering AND filtering, so we branch here 141 | // Alternative strategies here: https://github.com/dfahlander/Dexie.js/issues/297 142 | if (event.filters && event.filters["project"]) { 143 | query = table.where("project").equals(event.filters["project"]["value"]); 144 | } else if (event.globalFilter) { 145 | query = table.where("project").startsWithIgnoreCase(event.globalFilter) 146 | .or("user").startsWithIgnoreCase(event.globalFilter) 147 | .or("category").startsWithIgnoreCase(event.globalFilter); 148 | } else { 149 | query = table.orderBy(event.sortField); 150 | } 151 | 152 | query = query 153 | .offset(event.first) 154 | .limit(event.rows); 155 | 156 | if (event.sortOrder == -1) { 157 | query = query.reverse(); 158 | }; 159 | 160 | query.toArray((nextBlockOfTimes) => { 161 | // console.log("Loaded times: %s", JSON.stringify(nextBlockOfTimes)); 162 | this.allTimesheetData = nextBlockOfTimes; 163 | }); 164 | } 165 | 166 | 167 | ngOnInit() { 168 | this.contextMenu = [ 169 | { label: 'Debug', icon: 'fa fa-bug', command: (event) => this.onDebug(this.selectedRows) }, 170 | { label: 'Delete', icon: 'fa fa-close', command: (event) => this.onDelete(this.selectedRows) } 171 | ]; 172 | 173 | } 174 | 175 | onDebug(selectedRows: any) { 176 | console.log(JSON.stringify(selectedRows)); 177 | } 178 | 179 | onDelete(selectedRows: any) { 180 | this.allTimesheetData = this.allTimesheetData.filter((row) => { 181 | return !selectedRows.includes(row); 182 | }); 183 | } 184 | 185 | 186 | 187 | onEditComplete(editInfo) { 188 | let fieldChanged = editInfo.column.field; 189 | let newRowValues = editInfo.data; 190 | alert(`You edited ${fieldChanged} to ${newRowValues[fieldChanged]}`); 191 | } 192 | 193 | onRowSelect(rowInfo) { 194 | //console.log(JSON.stringify(rowInfo.data)); // or this.selectedRow 195 | } 196 | 197 | 198 | 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | #header { 2 | background-color: #F0F3F5; 3 | height: 60px; 4 | position: fixed; 5 | top: 0px; 6 | z-index: 99; 7 | width: 100%; 8 | } 9 | 10 | #top-logo { 11 | height: 60px; 12 | } 13 | 14 | #notifications { 15 | color: blue; 16 | float: right; 17 | } 18 | 19 | #sidenav, #sidenav-mini { 20 | height: 100%; 21 | position: fixed; 22 | top: 60px; 23 | padding-left: 0px; 24 | padding-top: 1em; 25 | background-color: #2D353C !important; 26 | } 27 | 28 | #content-body { 29 | padding-top: 55px; 30 | position: absolute; 31 | left: 190px; 32 | } 33 | 34 | #sidegutter { 35 | width: 340px; 36 | } 37 | 38 | #minigutter { 39 | display:none; 40 | } 41 | 42 | /* 43 | 44 | @media screen and (min-width: 40.063em) { 45 | */ 46 | 47 | @media only screen and (max-width: 1000px) { 48 | 49 | #sidegutter { 50 | display: none; 51 | } 52 | 53 | #minigutter { 54 | display: block; 55 | } 56 | 57 | #content-body { 58 | left: 80px; 59 | } 60 | 61 | } 62 | 63 | /* 64 | @media only screen and (max-width: 40em) { 65 | */ 66 | @media only screen and (max-width: 400px) { 67 | 68 | 69 | #sidegutter { 70 | display: none; 71 | } 72 | 73 | #minigutter { 74 | display: block; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 39 | 40 | 41 |
42 | 43 | 44 | 45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewChild, ElementRef, AfterViewInit} from '@angular/core'; 2 | import {MenuItem} from "primeng/primeng"; 3 | import {Menu} from "primeng/components/menu/menu"; 4 | import {ActivatedRoute, Router} from "@angular/router"; 5 | 6 | declare var jQuery :any; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.css'] 12 | }) 13 | export class AppComponent implements OnInit, AfterViewInit { 14 | 15 | menuItems: MenuItem[]; 16 | miniMenuItems: MenuItem[]; 17 | 18 | @ViewChild('bigMenu') bigMenu : Menu; 19 | @ViewChild('smallMenu') smallMenu : Menu; 20 | 21 | constructor(private router : Router) { 22 | 23 | } 24 | 25 | ngOnInit() { 26 | 27 | let handleSelected = function(event) { 28 | let allMenus = jQuery(event.originalEvent.target).closest('ul'); 29 | let allLinks = allMenus.find('.menu-selected'); 30 | 31 | allLinks.removeClass("menu-selected"); 32 | let selected = jQuery(event.originalEvent.target).closest('a'); 33 | selected.addClass('menu-selected'); 34 | } 35 | 36 | this.menuItems = [ 37 | {label: 'Dashboard', icon: 'fa fa-home', routerLink: ['/dashboard'], command: (event) => handleSelected(event)}, 38 | {label: 'All Times', icon: 'fa fa-calendar', routerLink: ['/alltimes'], command: (event) => handleSelected(event)}, 39 | {label: 'My Timesheet', icon: 'fa fa-clock-o', routerLink: ['/timesheet'], command: (event) => handleSelected(event)}, 40 | {label: 'Add Project', icon: 'fa fa-tasks', routerLink: ['/projects'], command: (event) => handleSelected(event)}, 41 | {label: 'My Profile', icon: 'fa fa-users', routerLink: ['/profile'], command: (event) => handleSelected(event)}, 42 | {label: 'Settings', icon: 'fa fa-sliders', routerLink: ['/settings'], command: (event) => handleSelected(event)}, 43 | ] 44 | 45 | this.miniMenuItems = []; 46 | this.menuItems.forEach( (item : MenuItem) => { 47 | let miniItem = { icon: item.icon, routerLink: item.routerLink } 48 | this.miniMenuItems.push(miniItem); 49 | }) 50 | 51 | } 52 | 53 | selectInitialMenuItemBasedOnUrl() { 54 | let path = document.location.pathname; 55 | let menuItem = this.menuItems.find( (item) => { return item.routerLink[0] == path }); 56 | if (menuItem) { 57 | let iconToFind = '.' + menuItem.icon.replace('fa ', ''); // make fa fa-home into .fa-home 58 | let selectedIcon = document.querySelector(`${iconToFind}`); 59 | jQuery(selectedIcon).closest('li').addClass('menu-selected'); 60 | } 61 | } 62 | 63 | ngAfterViewInit() { 64 | this.selectInitialMenuItemBasedOnUrl(); 65 | } 66 | 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { MenuModule, PanelModule, ChartModule, InputTextModule, ButtonModule, InputMaskModule, InputTextareaModule, EditorModule, CalendarModule, RadioButtonModule, FieldsetModule, DropdownModule, MultiSelectModule, ListboxModule, SpinnerModule, SliderModule, RatingModule, DataTableModule, ContextMenuModule, TabViewModule, DialogModule, StepsModule, ScheduleModule, TreeModule, GMapModule, DataGridModule, TooltipModule, ConfirmationService, ConfirmDialogModule, GrowlModule, DragDropModule, GalleriaModule } from 'primeng/primeng'; 7 | 8 | import { AppComponent } from './app.component'; 9 | import {RouterModule, Routes} from "@angular/router"; 10 | import { DashboardComponent } from './dashboard/dashboard.component'; 11 | import { StatisticComponent } from './statistic/statistic.component'; 12 | import { TimesheetComponent } from './timesheet/timesheet.component'; 13 | import { ProjectsComponent } from './projects/projects.component'; 14 | import { ProfileComponent } from './profile/profile.component'; 15 | import { SettingsComponent } from './settings/settings.component'; 16 | 17 | import { AlltimesComponent } from './alltimes/alltimes.component'; 18 | import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; 19 | import { FielderrorsComponent } from './fielderrors/fielderrors.component'; 20 | 21 | 22 | const appRoutes: Routes = [ 23 | { path: "", redirectTo: "/dashboard", pathMatch: "full" }, 24 | { path: "dashboard", component: DashboardComponent }, 25 | { path: "alltimes", component: AlltimesComponent }, 26 | { path: "timesheet", component: TimesheetComponent}, 27 | { path: "projects", component: ProjectsComponent}, 28 | { path: "profile", component: ProfileComponent}, 29 | { path: "settings", component: SettingsComponent}, 30 | ]; 31 | 32 | @NgModule({ 33 | declarations: [ 34 | AppComponent, 35 | DashboardComponent, 36 | StatisticComponent, 37 | TimesheetComponent, 38 | ProjectsComponent, 39 | AlltimesComponent, 40 | ProfileComponent, 41 | SettingsComponent, 42 | FielderrorsComponent 43 | ], 44 | imports: [ 45 | BrowserModule, 46 | FormsModule, 47 | ReactiveFormsModule, 48 | HttpModule, 49 | RouterModule.forRoot(appRoutes), 50 | BrowserAnimationsModule, 51 | MenuModule, 52 | PanelModule, 53 | ChartModule, 54 | InputTextModule, 55 | ButtonModule, 56 | InputMaskModule, 57 | InputTextareaModule, 58 | EditorModule, 59 | CalendarModule, 60 | RadioButtonModule, 61 | FieldsetModule, 62 | DropdownModule, 63 | MultiSelectModule, 64 | ListboxModule, 65 | SpinnerModule, 66 | SliderModule, 67 | RatingModule, 68 | DataTableModule, 69 | ContextMenuModule, 70 | TabViewModule, 71 | DialogModule, 72 | StepsModule, 73 | ScheduleModule, 74 | TreeModule, 75 | GMapModule, 76 | DataGridModule, 77 | TooltipModule, 78 | ConfirmDialogModule, 79 | GrowlModule, 80 | DragDropModule, 81 | GalleriaModule 82 | ], 83 | providers: [ ConfirmationService ], 84 | bootstrap: [AppComponent] 85 | }) 86 | export class AppModule { } 87 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'; 2 | import { UIChart } from "primeng/primeng"; 3 | import {interval} from 'rxjs'; 4 | 5 | 6 | 7 | const DEFAULT_COLORS = ['#3366CC', '#DC3912', '#FF9900', '#109618', '#990099', 8 | '#3B3EAC', '#0099C6', '#DD4477', '#66AA00', '#B82E2E', 9 | '#316395', '#994499', '#22AA99', '#AAAA11', '#6633CC', 10 | '#E67300', '#8B0707', '#329262', '#5574A6', '#3B3EAC'] 11 | 12 | 13 | @Component({ 14 | selector: 'at-dashboard', 15 | templateUrl: './dashboard.component.html', 16 | styleUrls: ['./dashboard.component.css'] 17 | }) 18 | export class DashboardComponent implements AfterViewInit { 19 | 20 | @ViewChild("mixedChart") mixedChart: UIChart; 21 | 22 | hoursByProject = [ 23 | { id: 1, name: 'Payroll App', hoursSpent: 8 }, 24 | { id: 2, name: 'Agile Times App', hoursSpent: 16 }, 25 | { id: 3, name: 'Point of Sale App', hoursSpent: 24 }, 26 | ] 27 | 28 | chartOptions = { 29 | title: { 30 | display: true, 31 | text: 'Hours By Project' 32 | }, 33 | legend: { 34 | position: 'bottom' 35 | }, 36 | }; 37 | 38 | pieLabels = this.hoursByProject.map((proj) => proj.name); 39 | 40 | pieData = this.hoursByProject.map((proj) => proj.hoursSpent); 41 | 42 | pieColors = this.configureDefaultColours(this.pieData); 43 | 44 | 45 | private configureDefaultColours(data: number[]): string[] { 46 | let customColours = [] 47 | if (data.length) { 48 | 49 | customColours = data.map((element, idx) => { 50 | return DEFAULT_COLORS[idx % DEFAULT_COLORS.length]; 51 | }); 52 | } 53 | 54 | return customColours; 55 | } 56 | 57 | 58 | 59 | hoursByProjectChartData = { 60 | labels: this.pieLabels, 61 | datasets: [ 62 | { 63 | data: this.pieData, 64 | backgroundColor: this.pieColors 65 | } 66 | ] 67 | } 68 | 69 | 70 | hoursByTeamChartData = { 71 | 72 | labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], 73 | datasets: [ 74 | { 75 | label: 'Dev Team', 76 | backgroundColor: DEFAULT_COLORS[0], 77 | data: [65, 59, 80, 55, 67, 73] 78 | }, 79 | { 80 | label: 'Ops Team', 81 | backgroundColor: DEFAULT_COLORS[1], 82 | data: [44, 63, 57, 90, 77, 70] 83 | } 84 | ] 85 | 86 | } 87 | 88 | hoursByTeamChartDataMixed = { 89 | 90 | labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], 91 | datasets: [ 92 | { 93 | label: 'Dev Team', 94 | type: 'bar', 95 | backgroundColor: DEFAULT_COLORS[0], 96 | data: [65, 59, 80, 55, 67, 73] 97 | }, 98 | { 99 | label: 'Ops Team', 100 | type: 'line', 101 | backgroundColor: DEFAULT_COLORS[1], 102 | data: [44, 63, 57, 90, 77, 70] 103 | } 104 | ] 105 | 106 | } 107 | 108 | onDataSelect(event) { 109 | 110 | let dataSetIndex = event.element._datasetIndex; 111 | let dataItemIndex = event.element._index; 112 | 113 | let labelClicked = this.hoursByTeamChartDataMixed.datasets[dataSetIndex].label; 114 | let valueClicked = this.hoursByTeamChartDataMixed.datasets[dataSetIndex].data[dataItemIndex]; 115 | 116 | alert(`Looks like ${labelClicked} worked ${valueClicked} hours`); 117 | } 118 | 119 | 120 | ngAfterViewInit() { 121 | interval(3000).subscribe(() => { 122 | 123 | var hoursByTeam = this.hoursByTeamChartDataMixed.datasets; 124 | var randomised = hoursByTeam.map((dataset) => { 125 | 126 | dataset.data = dataset.data.map((hours) => hours * (Math.random() * 2)); 127 | 128 | }); 129 | this.mixedChart.refresh(); 130 | }); 131 | 132 | } 133 | 134 | 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/app/fielderrors/fielderrors.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/src/app/fielderrors/fielderrors.component.css -------------------------------------------------------------------------------- /src/app/fielderrors/fielderrors.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {{ niceName}} is required 5 | {{ niceName}} must be {{ fieldErrors(fieldName).minlength.requiredLength }} characters 6 | {{ niceName}} must not exceed {{ fieldErrors(fieldName).maxlength.requiredLength }} characters 7 | 8 |
-------------------------------------------------------------------------------- /src/app/fielderrors/fielderrors.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { FormGroup } from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: 'at-fielderrors', 6 | templateUrl: './fielderrors.component.html', 7 | styleUrls: ['./fielderrors.component.css'] 8 | }) 9 | export class FielderrorsComponent implements OnInit { 10 | 11 | @Input("form") form: FormGroup; 12 | @Input("field") fieldName: string; 13 | @Input("nicename") niceName: string; 14 | 15 | 16 | 17 | constructor() { } 18 | 19 | ngOnInit() { 20 | } 21 | 22 | fieldErrors(field: string) { 23 | let controlState = this.form.controls[field]; 24 | return (controlState.dirty && controlState.errors) ? controlState.errors : null; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.css: -------------------------------------------------------------------------------- 1 | .profile { 2 | background-color: white; 3 | font-family: "Roboto"; 4 | } 5 | 6 | .header { 7 | padding: 1em; 8 | color: white; 9 | background-color: #0275D8; 10 | margin-bottom: 1em; 11 | } 12 | 13 | h2 { 14 | font-weight: bolder; 15 | font-size: xx-large; 16 | display: inline; 17 | } 18 | 19 | h3 { 20 | font-weight: lighter; 21 | font-size: xx-large; 22 | display: inline; 23 | } 24 | 25 | p-panel >>> .ui-panel-content { 26 | height: 320px; 27 | } 28 | 29 | p-panel img { 30 | width: 100%; 31 | } 32 | 33 | 34 | #drop-message { 35 | font-size: xx-large; 36 | color: #2D353C; 37 | background-color: lightgray; 38 | width: 100%; 39 | height: 85%; 40 | border: 3px solid #2D353C; 41 | text-align: center; 42 | } 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Profile Info 4 |

5 |

6 | Galleria Drag and Drop 7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | Drop Your Image Here 16 | 17 | 18 | 19 | 20 |
21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | import { 5 | DataTableModule, DragDropModule, FieldsetModule, GalleriaModule, GrowlModule, 6 | PanelModule 7 | } from "primeng/primeng"; 8 | import { NoopAnimationsModule } from "@angular/platform-browser/animations"; 9 | import createSpy = jasmine.createSpy; 10 | import { By } from "@angular/platform-browser"; 11 | 12 | describe('ProfileComponent', () => { 13 | let component: ProfileComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [ProfileComponent], 19 | imports: [GrowlModule, GalleriaModule, DragDropModule, PanelModule, NoopAnimationsModule] 20 | }) 21 | .compileComponents(); 22 | })); 23 | 24 | beforeEach(() => { 25 | fixture = TestBed.createComponent(ProfileComponent); 26 | component = fixture.componentInstance; 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('should stop the slideshow on starting drag', () => { 31 | 32 | let mockGalleria = { 33 | activeIndex: 2, 34 | stopSlideshow: createSpy('stopSlideshow') 35 | }; 36 | 37 | component.onDragStart(mockGalleria); 38 | expect(mockGalleria.stopSlideshow).toHaveBeenCalled(); 39 | }); 40 | 41 | it('should update the image on drop', () => { 42 | 43 | let mockGalleria = { 44 | activeIndex: 2, 45 | stopSlideshow: createSpy('stopSlideshow') 46 | }; 47 | 48 | component.onDragStart(mockGalleria); 49 | component.onPicDrop(); 50 | 51 | fixture.detectChanges(); 52 | 53 | expect(component.profileImage).toEqual("http://i.pravatar.cc/300?u=Mary"); 54 | let imgElement = fixture.debugElement.query(By.css('#profilePic')).nativeElement; 55 | expect(imgElement).toBeTruthy(); 56 | 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Galleria, Message } from "primeng/primeng"; 3 | 4 | @Component({ 5 | selector: 'at-profile', 6 | templateUrl: './profile.component.html', 7 | styleUrls: ['./profile.component.css'] 8 | }) 9 | export class ProfileComponent implements OnInit { 10 | 11 | profileImage: string; 12 | 13 | images = [ 14 | { source: "http://i.pravatar.cc/300?u=Anne", title: "Anne" }, 15 | { source: "http://i.pravatar.cc/300?u=Kerri", title: "Kerri" }, 16 | { source: "http://i.pravatar.cc/300?u=Mary", title: "Mary" }, 17 | { source: "http://i.pravatar.cc/300?u=Nancy", title: "Nancy" }, 18 | { source: "http://i.pravatar.cc/300?u=Peta", title: "Peta" }, 19 | ] 20 | 21 | selectedProfile: any; 22 | 23 | messages : Message[] = []; 24 | 25 | constructor() { } 26 | 27 | ngOnInit() { 28 | } 29 | 30 | onImageSelected(event) { 31 | console.log(JSON.stringify(event)); 32 | } 33 | 34 | onDragStart(galleria) { 35 | this.selectedProfile = this.images[galleria.activeIndex]; 36 | galleria.stopSlideshow(); 37 | } 38 | 39 | onPicDrop() { 40 | this.profileImage = this.selectedProfile.source; 41 | this.messages.push({ severity: "info", summary: "New Profile", detail: `Changed pic to ${this.selectedProfile.title}` }); 42 | } 43 | 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/app/projects/projects.component.css: -------------------------------------------------------------------------------- 1 | .ui-multiselect-label-container { 2 | background-color: red !important; 3 | } 4 | 5 | label { 6 | margin-top: 5px; 7 | } 8 | 9 | button { 10 | margin-top: 1em; 11 | } 12 | 13 | p-radioButton { 14 | display:block; 15 | margin: .7em; 16 | } 17 | 18 | p-listbox >>> .ui-listbox { 19 | width: 100%; 20 | } 21 | 22 | p-listbox >>> .ui-listbox-list-wrapper { 23 | height: 250px; 24 | } 25 | 26 | .avatar { 27 | float: left; 28 | margin: 5px; 29 | } 30 | 31 | .devName { 32 | font-size: xx-large; 33 | display:inline-block; 34 | margin:15px 10px 0 10px; 35 | min-height: 100px; 36 | } 37 | 38 | p-rating { 39 | font-size: xx-large; 40 | } 41 | 42 | #ratingLabel { 43 | margin-top: 20px; 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/app/projects/projects.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | {{dev.value}} 53 | 54 | 55 | 56 | 57 |
58 | 59 |
60 | 61 | 62 |
{{ projectForm.getRawValue() | json }}
63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/app/projects/projects.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ElementRef } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from "@angular/forms"; 3 | 4 | 5 | @Component({ 6 | selector: 'at-projects', 7 | templateUrl: './projects.component.html', 8 | styleUrls: ['./projects.component.css'] 9 | }) 10 | export class ProjectsComponent implements OnInit { 11 | 12 | projectForm: FormGroup; 13 | 14 | minProjectDate = new Date(); 15 | 16 | allDevs = [ 17 | 18 | { label: 'Jill', value: 'Jill Cool' }, 19 | { label: 'Joe', value: 'Joe Cool' }, 20 | { label: 'Mary', value: 'Mary Cool' }, 21 | { label: 'Susan', value: 'Susan Jones' }, 22 | { label: 'Phil', value: 'Phil Stephens' }, 23 | { label: 'Karen', value: 'Karen Phillips' }, 24 | { label: 'Chris', value: 'Chris Hampton' }, 25 | { label: 'Si', value: 'Si Chew' }, 26 | { label: 'Terri', value: 'Terri Smith' } 27 | 28 | ] 29 | 30 | 31 | constructor(private fb: FormBuilder) { } 32 | 33 | ngOnInit() { 34 | this.projectForm = this.fb.group({ 35 | projectId: ['', [Validators.required, Validators.minLength(5)]], 36 | description: ['My cool project', [Validators.required, Validators.maxLength(140)]], 37 | startDate: [new Date(), Validators.required], 38 | projectType: ['B'], 39 | selectedDevs: [[]], 40 | rating: [3] 41 | }) 42 | 43 | } 44 | 45 | hasFormErrors() { 46 | return !this.projectForm.valid; 47 | } 48 | 49 | onSubmit() { 50 | alert(JSON.stringify(this.projectForm.value)); 51 | } 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/app/rxjs-operators.ts: -------------------------------------------------------------------------------- 1 | 2 | // Not sure what operator you need? 3 | // Head over here: 4 | // https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/which-instance.md 5 | 6 | 7 | // Observable class extensions 8 | import 'rxjs/add/observable/of'; 9 | import 'rxjs/add/observable/from'; 10 | import 'rxjs/add/observable/range'; 11 | 12 | import 'rxjs/add/observable/interval'; 13 | import 'rxjs/add/observable/timer'; 14 | 15 | // Observable operators 16 | import 'rxjs/add/operator/map'; 17 | import 'rxjs/add/operator/do'; 18 | import 'rxjs/add/operator/catch'; 19 | import 'rxjs/add/operator/switchMap'; 20 | import 'rxjs/add/operator/mergeMap'; 21 | import 'rxjs/add/operator/filter'; 22 | import 'rxjs/add/operator/debounceTime'; 23 | import 'rxjs/add/operator/distinctUntilChanged'; 24 | import 'rxjs/add/operator/timeInterval'; 25 | 26 | // Subscribe once and dispose: https://stackoverflow.com/questions/28007777/rxjs-create-subscribe-once-and-dispose-method 27 | import 'rxjs/add/operator/first'; 28 | import 'rxjs/add/operator/take'; 29 | import 'rxjs/add/operator/withLatestFrom'; 30 | -------------------------------------------------------------------------------- /src/app/settings/settings.component.css: -------------------------------------------------------------------------------- 1 | p-panel >>> .ui-panel { 2 | margin-top: 1em; 3 | width: 98%; 4 | min-height: 300px; 5 | } 6 | 7 | .ui-g { 8 | padding: 4px; 9 | } 10 | 11 | #statistic { 12 | 13 | width: 300px; 14 | color: white; 15 | text-align: center; 16 | font-family: "Roboto"; 17 | background-color: #00ACAC; 18 | } 19 | 20 | .icon { 21 | font-size: 80px; 22 | margin: 0px; 23 | padding: 5px; 24 | background:rgba(0,0,0,.1); 25 | } 26 | 27 | .data { 28 | padding: 1em; 29 | vertical-align: middle; 30 | } 31 | 32 | .value { 33 | font-size: 40px; 34 | } 35 | 36 | .label { 37 | text-transform: uppercase; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/settings/settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
20
21 |
Days Uptime
22 |
23 |
24 |
25 | 26 |
-------------------------------------------------------------------------------- /src/app/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'at-settings', 5 | templateUrl: './settings.component.html', 6 | styleUrls: ['./settings.component.css'] 7 | }) 8 | export class SettingsComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/statistic/statistic.component.css: -------------------------------------------------------------------------------- 1 | .statistic { 2 | margin: 1em; 3 | min-width: 200px; 4 | color: white; 5 | text-align: center; 6 | font-family: "Roboto"; 7 | } 8 | 9 | .icon { 10 | font-size: 70px; 11 | margin: 0px; 12 | padding: 5px; 13 | background:rgba(0,0,0,.1); 14 | } 15 | 16 | .data { 17 | padding: 1em; 18 | vertical-align: middle; 19 | } 20 | 21 | .value { 22 | font-size: 40px; 23 | } 24 | 25 | .label { 26 | text-transform: uppercase; 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/statistic/statistic.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 |
10 | {{ value }} 11 |
12 | 13 |
14 | {{ label }} 15 |
16 | 17 |
18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /src/app/statistic/statistic.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'at-statistic', 5 | templateUrl: './statistic.component.html', 6 | styleUrls: ['./statistic.component.css'] 7 | }) 8 | export class StatisticComponent implements OnInit { 9 | 10 | @Input() icon : string; 11 | @Input() label : string; 12 | @Input() value: string; 13 | @Input() colour: string; 14 | 15 | constructor() { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/timesheet/sample.projects.data.ts: -------------------------------------------------------------------------------- 1 | export class SampleProjectsData { 2 | 3 | static projects = [ 4 | { 5 | "label": "Projects", 6 | "data": "proj", 7 | "expandedIcon": "fa fa-folder-open", 8 | "collapsedIcon": "fa fa-folder", 9 | "selectable": false, 10 | "children": [{ 11 | "label": "Agile Times", 12 | "selectable": false, 13 | "data": "agile", 14 | "expandedIcon": "fa fa-folder-open", 15 | "collapsedIcon": "fa fa-folder", 16 | "children": [ 17 | {"label": "Frontend", "icon": "fa fa-chrome", "data": "fe"}, 18 | {"label": "Backend", "icon": "fa fa-cloud", "data": "be"}, 19 | {"label": "Operations", "icon": "fa fa-cogs", "data": "ops"} 20 | ] 21 | }, 22 | { 23 | "label": "Mobile App", 24 | "data": "mobile", 25 | "expandedIcon": "fa fa-folder-open", 26 | "collapsedIcon": "fa fa-folder", 27 | "selectable": false, 28 | "children": [ 29 | {"label": "Frontend", "icon": "fa fa-chrome", "data": "fe"}, 30 | {"label": "Backend", "icon": "fa fa-cloud", "data": "be"}, 31 | {"label": "Operations", "icon": "fa fa-cogs", "data": "ops"} 32 | ] 33 | }] 34 | 35 | 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/app/timesheet/timesheet.component.css: -------------------------------------------------------------------------------- 1 | .timesheet { 2 | background-color: white; 3 | font-family: "Roboto"; 4 | } 5 | 6 | .header { 7 | padding: 1em; 8 | color: white; 9 | background-color: #0275D8; 10 | margin-bottom: 1em; 11 | } 12 | 13 | .dialogBody { 14 | height: 400px; 15 | } 16 | 17 | h2 { 18 | font-weight: bolder; 19 | font-size: xx-large; 20 | display: inline; 21 | } 22 | 23 | h3 { 24 | font-weight: lighter; 25 | font-size: xx-large; 26 | display: inline; 27 | } 28 | 29 | .tabs >>> li { 30 | width: 19%; 31 | } 32 | 33 | .timesheet-grid >>> .ui-datatable { 34 | margin: 1em; 35 | } 36 | 37 | p-schedule >>> .calendar { 38 | height: 250px; 39 | } 40 | 41 | 42 | p-gmap >>> .gmap { 43 | width:100%; 44 | height: 300px; 45 | } 46 | 47 | p-steps >>> .ui-steps-item { 48 | width: 25%; 49 | } 50 | 51 | p-dataGrid >>> .ui-panel { 52 | border: 0px; 53 | } 54 | 55 | /** 56 | p-dataGrid >>> .ui-panel-titlebar { 57 | font-size: smaller; 58 | background-color: #F15B2A !important; 59 | text-align: center; 60 | height: 35px; 61 | } 62 | */ 63 | 64 | p-dataGrid >>> .ui-panel-content { 65 | padding: 0px; 66 | } 67 | 68 | p-dataGrid >>> .ui-panel-content img { 69 | display: block; 70 | margin-left: auto; 71 | margin-right: auto; 72 | width: 144px; 73 | } 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/app/timesheet/timesheet.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ day }} 4 |

5 |

6 | {{ dateAndMonth }} 7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 |
31 | 32 | 33 | 34 | 35 |
36 | 37 | 40 | 41 | 42 | 43 |
44 |
45 | 46 | 48 | 49 |
50 |
51 | 52 | 54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/app/timesheet/timesheet.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MenuItem, TreeNode, ConfirmationService, Message } from "primeng/primeng"; 3 | import { SampleProjectsData } from "app/timesheet/sample.projects.data"; 4 | import { SamplePeopleData } from "app/timesheet/sample.people.data"; 5 | 6 | declare var moment: any; 7 | 8 | declare var google: any; 9 | 10 | export enum PageNames { 11 | TimePage, 12 | ProjectPage, 13 | PlacePage, 14 | PeoplePage 15 | } 16 | 17 | 18 | @Component({ 19 | selector: 'at-timesheet', 20 | templateUrl: './timesheet.component.html', 21 | styleUrls: ['./timesheet.component.css'] 22 | }) 23 | export class TimesheetComponent { 24 | 25 | private userTimeData = [ 26 | 27 | { day: "Monday", startTime: '9:00', endTime: '17:00', project: 'Agile Times', category: "Frontend" }, 28 | { day: "Tuesday", startTime: '9:00', endTime: '17:00', project: 'Payroll App', category: "Backend" }, 29 | { day: "Wednesday", startTime: '9:00', endTime: '17:00', project: 'Point of Sale App', category: "Operations" }, 30 | { day: "Thursday", startTime: '9:00', endTime: '17:00', project: 'Mobile App', category: "Planning" }, 31 | { day: "Friday", startTime: '9:00', endTime: '17:00', project: 'Agile Times', category: "Requirements" }, 32 | 33 | ] 34 | 35 | daysOfWeek = [ 36 | "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" 37 | ] 38 | 39 | displayEditDialog = false; 40 | 41 | PageNames = PageNames; 42 | 43 | dialogPageIndex = PageNames.TimePage; 44 | 45 | dialogPages: MenuItem[] = [ 46 | { label: "Time" }, 47 | { label: "Project" }, 48 | { label: "Place" }, 49 | { label: "People" } 50 | ]; 51 | 52 | private headerConfig = { 53 | left: 'prev,next today', 54 | center: 'title', 55 | right: 'month,agendaWeek,agendaDay' 56 | }; 57 | 58 | private events = [ 59 | { 60 | title: 'Recent Work', 61 | start: moment().format(), // '2017-06-02 07:00:00' 62 | end: moment().add(1, "hour").format() 63 | } 64 | ] 65 | 66 | projectsTree: TreeNode[] = SampleProjectsData.projects; 67 | 68 | selectedProject: TreeNode; 69 | 70 | private mapOptions = { 71 | 72 | center: { lat: -33.8688, lng: 151.2093 }, 73 | zoom: 5 74 | }; 75 | 76 | private mapOverlays = [ 77 | new google.maps.Marker({ position: { lat: -35.3075, lng: 149.124417 }, title: "Canberra Office" }), 78 | new google.maps.Marker({ position: { lat: -33.8688, lng: 151.2093 }, title: "Sydney Office" }), 79 | new google.maps.Marker({ position: { lat: -37.813611, lng: 144.963056 }, title: "Melbourne Office" }), 80 | new google.maps.Marker({ position: { lat: -28.016667, lng: 153.4 }, title: "Gold Coast Office" }) 81 | ]; 82 | 83 | people = SamplePeopleData.people; 84 | 85 | 86 | messages: Message[] = []; 87 | 88 | constructor(private confirmationService: ConfirmationService) { 89 | 90 | } 91 | 92 | getTimesForDay(tabName: string) { 93 | return this.userTimeData.filter((row) => { 94 | return row.day == tabName; 95 | }) 96 | } 97 | 98 | day = "Monday"; 99 | dateAndMonth = moment().day(this.day).format("MMMM Do, YYYY"); 100 | 101 | onChangeTabs(event) { 102 | let index = event.index; 103 | this.day = this.daysOfWeek[index]; 104 | this.dateAndMonth = moment().day(this.day).format("MMMM Do, YYYY"); 105 | } 106 | 107 | cancelDialog() { 108 | 109 | this.confirmationService.confirm({ 110 | header: 'Cancel Time Creation', 111 | message: 'Cancel all changes. Are you sure?', 112 | accept: () => { 113 | this.displayEditDialog = false; 114 | this.messages.push({ severity: 'info', summary: 'Edits Cancelled', detail: 'No changes were saved' }); 115 | }, 116 | reject: () => { 117 | this.messages.push({ severity: 'warn', summary: 'Cancelled the Cancel', detail: 'Please continue your editing' }); 118 | console.log("False cancel. Just keep editing."); 119 | } 120 | }); 121 | 122 | 123 | } 124 | 125 | onMarkerClick(markerEvent) { 126 | 127 | let markerTitle = markerEvent.overlay.title; 128 | let markerPosition = markerEvent.overlay.position; 129 | 130 | alert(`You clicked on ${markerTitle} at ${markerPosition}`); 131 | 132 | markerEvent.map.panTo(markerPosition); 133 | markerEvent.map.setZoom(12); 134 | 135 | } 136 | 137 | saveNewEntry() { 138 | this.displayEditDialog = false; 139 | this.messages.push({ severity: 'success', summary: 'Entry Created', detail: 'Your entry has been created' }); 140 | } 141 | 142 | 143 | 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/data/hoursByTeam.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Dev Team", 4 | "monthlyHours": { 5 | "Jan": 65, 6 | "Feb": 59, 7 | "Mar": 80, 8 | "Apr": 55, 9 | "May": 67, 10 | "Jun": 73 11 | } 12 | }, 13 | { 14 | "name": "Ops Team", 15 | "monthlyHours": { 16 | "Jan": 44, 17 | "Feb": 63, 18 | "Mar": 57, 19 | "Apr": 90, 20 | "May": 77, 21 | "Jun": 70 22 | } 23 | }, 24 | { 25 | "name": "Management Team", 26 | "monthlyHours": { 27 | "Jan": 8, 28 | "Feb": 12, 29 | "Mar": 31, 30 | "Apr": 20, 31 | "May": 16, 32 | "Jun": 11 33 | } 34 | } 35 | ] -------------------------------------------------------------------------------- /src/assets/data/people.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 101, 4 | "name": "Glen Smith", 5 | "avatar": "/profiles/glen.jpg", 6 | "role": "Developer", 7 | "times": [ 8 | 9 | ] 10 | }, 11 | { 12 | "id": 102, 13 | "name": "Mary Cool", 14 | "avatar": "/profiles/mary.jpg", 15 | "role": "Developer", 16 | "times": [ 17 | 18 | ] 19 | } 20 | ] 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/data/projects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 201, 4 | "name": "Payroll System" 5 | }, 6 | { 7 | "id": 202, 8 | "name": "Inventory System" 9 | } 10 | ] 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/data/teams.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 301, 4 | "name": "Front End Developers", 5 | "budgetHours" : 4000, 6 | "hoursSpent": 2600, 7 | "people": [ 8 | 9 | ] 10 | }, 11 | { 12 | "id": 302, 13 | "name": "Back End Developers", 14 | "budgetHours" : 6000, 15 | "hoursSpent": 3700, 16 | "people": [ 17 | 18 | ] 19 | } 20 | ] 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/img/logo-clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Agile Times 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenasmith/pluralsight-primeng/8e98e519a71cf70f8927fffe2289bbbaf4dac3f8/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Agile Times 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Loading... 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { environment } from './environments/environment'; 4 | import { AppModule } from './app/app.module'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic().bootstrapModule(AppModule); 11 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular-CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import '~primeicons/primeicons.css'; 2 | 3 | /* You can add global styles to this file, and also import other style files */ 4 | body { 5 | background-color: #D9E0E7 !important; 6 | margin: 0px; 7 | font-size: 1em !important; 8 | } 9 | 10 | .ui-panel-titlebar { 11 | background-color: #2F8EE5 !important; 12 | color: white !important; 13 | } 14 | 15 | .ui-menu { 16 | width: auto !important; 17 | margin: 0px !important; 18 | border: none !important; 19 | background-color: #2D353C !important; 20 | } 21 | 22 | .ui-menu-list { 23 | background-color: black; 24 | } 25 | 26 | .ui-menuitem-text { 27 | color: white !important; 28 | } 29 | 30 | .ui-menuitem-icon { 31 | color: white !important; 32 | font-size: large !important; 33 | margin-right: 1em; 34 | } 35 | 36 | body .ui-menu .ui-menuitem :hover { 37 | background-color: #2F8EE5 !important; 38 | } 39 | 40 | .menu-selected { 41 | background-color: #F15B2A !important; 42 | color: #ffffff; 43 | } 44 | 45 | /* Debugging Layouts 46 | @media screen and (min-width: 64.063em) { 47 | .ui-lg-3 { 48 | background-color: green; 49 | } 50 | } 51 | 52 | @media screen and (min-width: 40.063em) { 53 | .ui-md-6 { 54 | background-color: yellow; 55 | } 56 | } 57 | 58 | @media screen and (max-width: 40em) { 59 | .ui-sm-12 { 60 | background-color: palevioletred; 61 | } 62 | } 63 | */ 64 | 65 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "es2016", 9 | "dom" 10 | ], 11 | "mapRoot": "./", 12 | "module": "es2015", 13 | "moduleResolution": "node", 14 | "outDir": "../dist/out-tsc", 15 | "sourceMap": true, 16 | "target": "es5", 17 | "typeRoots": [ 18 | "../node_modules/@types" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": true, 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /xample-scripts/1.intro.md: -------------------------------------------------------------------------------- 1 | # Intro Module 2 | 3 | ## Overview of PrimeNG 4 | 5 | See Google Doc 6 | 7 | ## What's in the Box 8 | 9 | See Google Doc 10 | 11 | ## Getting Setup 12 | 13 | I'm presuming your using [angular-cli](https://github.com/angular/angular-cli) - and that tool churns hard. 14 | 15 | npm install -g @angular/cli 16 | 17 | At this moment in time, it works like this: 18 | 19 | Setup your project: 20 | 21 | ng new agile-times 22 | 23 | npm install primeng --save 24 | npm install font-awesome --save 25 | 26 | Edit your .angular-cli.json in your root dir to drag in the styles: 27 | 28 | "styles": [ 29 | "../node_modules/font-awesome/css/font-awesome.css", 30 | "../node_modules/primeng/resources/primeng.css" 31 | "styles.css", 32 | ], 33 | 34 | 35 | ## Migrating Between Prime Versions (upgrading to the current version from the course version) 36 | 37 | A word on semantic versioning. Run NPM outdated to see what's changed. I'll keep the GitHub repo up to speed. 38 | 39 | npm outdated 40 | 41 | ## Themes 42 | 43 | Themes are build in with [good docs](https://www.primefaces.org/primeng/#/theming) based around [jQueryUI Themeroller](https://jqueryui.com/themeroller/). You can also purchase commercial themes. 44 | 45 | In this course we're using the free Bootstrap theme - but it won't look like the cliche Bootstrap site you're thinking of. 46 | 47 | Themes are configured just through CSS style application in your angular-cli.json. Here's what you need to know up front (I've added font-awesome to the mix) 48 | 49 | "styles": [ 50 | "../node_modules/font-awesome/css/font-awesome.css", 51 | "../node_modules/primeng/resources/primeng.css", 52 | "../node_modules/primeng/resources/themes/bootstrap/theme.css", 53 | "styles.css" 54 | ], 55 | 56 | 57 | 58 | ## Your First Component: Panel 59 | 60 | The three step process of: 61 | 62 | 1. Find and import the component module (sometimes more than one) 63 | 1. Add markup to your page to invoke the component 64 | 1. (Optionally) wire up to a component backing data properties or methods 65 | 66 | 67 | 68 | 69 |
70 | 71 | 72 |
73 | 74 |
75 | 76 | 77 |
78 | 79 |
80 | 81 | 82 | 83 | ## Styling Your First Component - into the /deep/ of CSS 84 | 85 | You can use /deep/ to style inside a panel (or the >>> selector which has now replaced it) 86 | 87 | p-panel >>> .ui-panel { 88 | margin-top: 1em; 89 | width: 98%; 90 | min-height: 300px; 91 | } 92 | 93 | ## Making use of Grid Systems 94 | 95 | Specify the grid using a combination of ui-g- ui-lg- ui-md- ui-sm- 96 | 97 | Drag your browser or use F12 tools to change to mobile view. 98 | 99 | 100 | 101 |
102 | 103 | 104 |
105 | 106 |
107 | 108 | 109 |
110 | 111 |
112 | 113 | 114 | ## Making use of Font Awesome 115 | 116 | Once you install font-awesome, you have access to a huge collection of CSS fonts. Using they is a matter of styling an element with the appropriate class: 117 | 118 | 119 | 120 | which I could style for size and color with font-size: 50px or color: red or whatever. 121 | 122 | Because I'm styling the I element, I need to add both fa and fa-cloud-download, but many Prime components support an icon element which you can specify the name of the icon `fa-cloud-download` directly. We'll see that shortly in the menu. 123 | 124 | 125 | ## Building a Stats Component 126 | 127 | You can, of course, nest grids within grids: 128 | 129 |
130 | 131 | 132 |
133 | 134 |
135 | 136 |
137 | 138 |
139 |
20
140 |
Days Uptime
141 |
142 |
143 | 144 | 145 |
146 | 147 | Challenge: Make your own statistics component that makes the labels and icons configurable 148 | 149 | ## Data Backed Components - The Main Menu 150 | 151 | Our p-panel isn't Data-backed, but some components are. The menu that you see in the application is a p-menu component: 152 | 153 | If you were using this component in your app, you'd first add it to your imports. 154 | 155 | MenuModule 156 | 157 | Then you'd add the markup: 158 | 159 | 160 | 161 | But in this case, the menu is populated by a backing model: 162 | 163 | private menuItems: MenuItem[]; 164 | 165 | Remember, types aren't really a thing in JavaScript, they will be compiled away. But they are awesome for developer happiness. 166 | 167 | this.menuItems = 168 | {label: 'Dashboard', icon: 'fa-home', routerLink: ['/dashboard'] } 169 | {label: 'All Timesheets', icon: 'fa-calendar', routerLink: ['/alltimes'] }, 170 | {label: 'My Timesheet', icon: 'fa-clock-o', routerLink: ['/mytimes'] }, 171 | {label: 'My Projects', icon: 'fa-tasks', routerLink: ['/projects'] }, 172 | {label: 'My Profile', icon: 'fa-users', routerLink: ['/profile'] }, 173 | {label: 'Settings', icon: 'fa-sliders', routerLink: ['/settings'] } 174 | ] 175 | 176 | That's an example of that icon property we mentioned earlier! 177 | 178 | 179 | ## Architecture Map of Agile Times - Source Code Layout 180 | 181 | Show each menu of the sample app mapping to a component in the matching directory property. 182 | 183 | Our components will be pre-created in the sample app, ready for you to add your own goodness. 184 | 185 | Be cool to have a plunker to experiment with too. TODO make one! 186 | 187 | 188 | -------------------------------------------------------------------------------- /xample-scripts/2.charts.md: -------------------------------------------------------------------------------- 1 | 2 | # Charts 3 | 4 | 5 | 6 | ## Intro the Chart Module 7 | 8 | 9 | import { ChartModule, MenuModule, PanelModule, DataTableModule} from 'primeng/primeng'; 10 | 11 | imports: [ 12 | ... 13 | ChartModule, 14 | ... 15 | ], 16 | 17 | 18 | ## Install Chart.js 19 | 20 | Install supporting library 21 | 22 | npm install chart.js --save 23 | 24 | Update the scripts section to run Chart.js on index.html startup 25 | 26 | "scripts": [ 27 | "../node_modules/chart.js/dist/Chart.js" 28 | ], 29 | 30 | 31 | ## Creating your first Pie chart 32 | 33 | 34 | 35 | 36 | 37 | ## Structuring the Data 38 | 39 | 40 | But you need the data in a format like this: 41 | 42 | private hoursByProjectChartData = { 43 | labels: ['Payroll App', 'Agile Times App', 'Point of Sale App'], 44 | datasets: [ 45 | { 46 | data: [8, 16, 24], 47 | backgroundColor: [ 48 | "red", 49 | "blue", 50 | "yellow" 51 | ], 52 | 53 | } 54 | ] 55 | }; 56 | 57 | But in reality, you probably have structured JSON data from a backend service: 58 | 59 | 60 | private hoursByProject = [ 61 | {id: 1, name: 'Payroll App', hoursSpent: 8}, 62 | {id: 2, name: 'Agile Times App', hoursSpent: 16}, 63 | {id: 3, name: 'Point of Sale App', hoursSpent: 24}, 64 | ] 65 | 66 | 67 | So you'll want to remap it: 68 | 69 | 70 | private pieData = this.hoursByProject.map((proj) => proj.hoursSpent); 71 | private pieLabels = this.hoursByProject.map((proj) => proj.name); 72 | 73 | Then run the demo: 74 | 75 | 76 | 77 | ## Fixing the Colours 78 | 79 | And those colours need to change to: 80 | 81 | private pieColors = this.configureDefaultColours(this.pieData); 82 | 83 | But how does the magic work? An array of colours to pick from for each new data item: 84 | 85 | const DEFAULT_COLORS = [ 86 | '#6C76AF', '#EFA64C', '#00ACAC', '#2F8EE5', 87 | '#F15B2A', '#A62E5C', '#2A9FBC', '#9BC850', 88 | '#404040', '#675BA7' 89 | ] 90 | 91 | Then I'll write some code to slurp out those colours: 92 | 93 | private configureDefaultColours(data: number[]): string[] { 94 | let customColours = [] 95 | if (data.length) { 96 | customColours = data.map((element, idx) => { 97 | return DEFAULT_COLORS[idx % DEFAULT_COLORS.length]; 98 | }); 99 | } 100 | 101 | return customColours; 102 | } 103 | 104 | Which gives us something much more please, but still readable 105 | 106 | private hoursByProjectChartData = { 107 | labels: this.pieLabels, 108 | datasets: [ 109 | { 110 | data: this.pieData, 111 | backgroundColor: this.pieColors, 112 | } 113 | ] 114 | }; 115 | 116 | ## Pies to Donuts 117 | 118 | Same datamodel. So it's a one-liner change: 119 | 120 | 121 | 122 | Same for polarArea 123 | 124 | 125 | 126 | ## Moving to Bar Charts 127 | 128 | Bars and Line charts work around series data. We have monthly hours by team: 129 | 130 | private hoursByTeam = [ 131 | 132 | { 133 | name: 'Dev Team', 134 | monthlyHours: { 135 | 'Jan': 65, 136 | 'Feb': 59, 137 | 'Mar': 80, 138 | 'Apr': 55, 139 | 'May': 67, 140 | 'Jun': 73, 141 | } 142 | }, 143 | { 144 | name: 'Ops Team', 145 | monthlyHours: { 146 | 'Jan': 44, 147 | 'Feb': 63, 148 | 'Mar': 57, 149 | 'Apr': 90, 150 | 'May': 77, 151 | 'Jun': 70, 152 | 153 | } 154 | } 155 | 156 | But we want to end up with series data: 157 | 158 | private hoursByTeamChartData = { 159 | labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], 160 | datasets: [ 161 | { 162 | label: 'Dev Team', 163 | backgroundColor: DEFAULT_COLORS[0], 164 | data: [65, 59, 80, 55, 67, 73] 165 | }, 166 | { 167 | label: 'Ops Team', 168 | backgroundColor: DEFAULT_COLORS[1], 169 | data: [44, 63, 57, 90, 77, 70] 170 | } 171 | ] 172 | } 173 | 174 | We'll need to transform our JSON: 175 | 176 | 177 | private tranformHoursByTeamDataIntoChartData(hoursByTeamData) { 178 | 179 | let labels = Object.keys(hoursByTeamData[0].monthlyHours); 180 | let dataSets = hoursByTeamData.map( (nextTeam, idx) => { 181 | return { 182 | label: nextTeam.name, 183 | backgroundColor: DEFAULT_COLORS[idx % DEFAULT_COLORS.length], 184 | data: Object.keys(nextTeam.monthlyHours).map(key => nextTeam.monthlyHours[key]) 185 | } 186 | }); 187 | 188 | return { 189 | labels: labels, 190 | datasets: dataSets 191 | } 192 | } 193 | 194 | 195 | ## Line and Area Charts 196 | 197 | Again, a one-liner: 198 | 199 | 200 | 201 | 202 | If you don't want the fills - line, not area: 203 | 204 | 205 | return { 206 | label: nextTeam.name, 207 | fill: false, 208 | backgroundColor: DEFAULT_COLORS[idx % DEFAULT_COLORS.length], 209 | data: Object.keys(nextTeam.monthlyHours).map(key => nextTeam.monthlyHours[key]) 210 | } 211 | 212 | ## Mixing Chart Data 213 | 214 | Chart type is set to bar, but then each data series can set it's preferred type: 215 | 216 | 217 | private hoursByTeamChartData = { 218 | labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], 219 | datasets: [ 220 | { 221 | type: 'line', 222 | fill: false, 223 | label: 'Dev Team', 224 | backgroundColor: DEFAULT_COLORS[0], 225 | data: [65, 59, 80, 55, 67, 73] 226 | }, 227 | { 228 | type: 'bar', 229 | label: 'Ops Team', 230 | backgroundColor: DEFAULT_COLORS[1], 231 | data: [44, 63, 57, 90, 77, 70] 232 | } 233 | ] 234 | } 235 | 236 | So let's create a fresh tag for it: 237 | 238 | 239 | 240 | 241 | 242 | So let's re-use our current transformer to popular our Mixed Chart data: 243 | 244 | private hoursByTeamChartMixedData = {}; 245 | 246 | constructor() { 247 | 248 | let mixedData = this.tranformHoursByTeamDataIntoChartData(this.hoursByTeam); 249 | 250 | mixedData.datasets[0].type = 'bar'; 251 | mixedData.datasets[1].type = 'line'; 252 | mixedData.datasets[2].type = 'line'; 253 | mixedData.datasets[2].fill = true; 254 | 255 | this.hoursByTeamChartMixedData = mixedData; 256 | 257 | } 258 | 259 | ## Added interactivity with (onDataSelect) 260 | 261 | We can implement (onDataSelect): 262 | 263 | 265 | 266 | For now, let's just show an alert box. We'll circle back later: 267 | 268 | private onMixedClick(event) { 269 | 270 | 271 | let labelClicked = this.hoursByTeamChartMixedData.datasets[event.element._datasetIndex].label; 272 | let valueClicked = this.hoursByTeamChartMixedData.datasets[event.element._datasetIndex].data[event.element._index]; 273 | 274 | alert(`Looks like ${labelClicked} worked ${valueClicked} hours`); 275 | 276 | } 277 | 278 | This gives us access to: 279 | 280 | //event.dataset = Selected dataset 281 | //event.element = Selected element 282 | //event.element._datasetIndex = Which dataset series was clicked on - ie. which array was clicked (0 indexed) 283 | //event.element._index = Which element within that array was clicked (0 indexed) 284 | 285 | 286 | ## Deeper Customisation 287 | 288 | There's more treasure in [Chart.js](http://www.chartjs.org/docs/) if you want to dig further: 289 | 290 | 291 | private chartOptions = { 292 | title: { 293 | display: true, 294 | text: 'Hours By Team' 295 | }, 296 | legend: { 297 | position: 'left' 298 | }, 299 | responsive: false, 300 | animation : false 301 | } 302 | 303 | Plus add them to the tag you care about: 304 | 305 | 307 | 308 | 309 | ## Realtime Charting 310 | 311 | 312 | 313 | 314 | Bind to the element: 315 | 316 | 319 | 320 | Grab a handle to the element: 321 | 322 | @ViewChild("mixedChart") mixedChart : UIChart; 323 | 324 | And the import: 325 | 326 | import { UIChart } from 'primeng/primeng'; 327 | 328 | Implement after view init: 329 | 330 | import {Component, OnInit, ViewChild, AfterViewInit} from '@angular/core'; 331 | export class DashboardComponent implements OnInit, AfterViewInit { 332 | 333 | Implement the view init code to make the magic happen: 334 | 335 | Observable.interval(3000).timeInterval().subscribe(() => { 336 | 337 | var hoursByTeam = this.hoursByTeamChartMixedData.datasets; 338 | var randomised = hoursByTeam.map( (dataset) => { 339 | 340 | dataset.data = dataset.data.map( (hours) => hours * Math.random() ); 341 | 342 | }); 343 | this.mixedChart.refresh(); 344 | }); 345 | 346 | If you're changing the whole chart object, you'll need to call: 347 | 348 | this.mixedChart.reinit(); 349 | 350 | ## Summarise and take action 351 | 352 | -------------------------------------------------------------------------------- /xample-scripts/3.forms.md: -------------------------------------------------------------------------------- 1 | 2 | # Forms 3 | 4 | Reactive is the way to go. Much more configurable than Template Driven. 5 | 6 | Import the ReactiveFormsModule before you start! 7 | 8 | imports: [ 9 | BrowserModule, 10 | FormsModule, 11 | ReactiveFormsModule, 12 | 13 | 14 | ## Create a basic reactive form 15 | 16 | Note to get theme and validation styling, we need to add a `pInputText` directive. 17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | ## With some backing helpers 44 | 45 | So our validate trips nicely. 46 | 47 | private projectForm : FormGroup; 48 | 49 | constructor(private fb : FormBuilder) { } 50 | 51 | ngOnInit() { 52 | this.projectForm = this.fb.group({ 53 | projectId: ['', [Validators.required, Validators.minLength(5)]] 54 | }) 55 | } 56 | 57 | hasFormErrors() { 58 | return !this.projectForm.valid; 59 | } 60 | 61 | fieldErrors(field : string) { 62 | let controlState = this.projectForm.controls[field]; 63 | return (controlState.errors && controlState.dirty) ? controlState.errors : null; 64 | } 65 | 66 | 67 | ## Using Validation Styles 68 | 69 | 70 | 71 |
73 | 74 | Project ID is required 75 | Project ID must be 5 characters 76 |
77 | 78 | 79 | ## Applying Platform Styling 80 | 81 | Notice the button is not styled quite right? pButton fixes that. And gives us icon support too: 82 | 83 | imports: [ 84 | BrowserModule, 85 | ButtonModule, 86 | 87 | 88 | 89 | 90 | ## Masked Forms 91 | 92 | First import the module 93 | 94 | imports: [ 95 | BrowserModule, 96 | FormsModule, 97 | ReactiveFormsModule, 98 | InputMaskModule, 99 | 100 | 101 | Specify masks as: 102 | 103 | a - Alpha character (A-Z,a-z) 104 | 9 - Numeric character (0-9) 105 | * - Alpha numberic character (A-Z,a-z,0-9) 106 | 107 | We'll keep the same layout: 108 | 109 | 110 | 111 | You'll notice some glitches in the current mask save. But they still prevent clicking on save once the cycle finishes. 112 | 113 | ## Added in a basic description with TextArea 114 | 115 | First import the module 116 | 117 | imports: [ 118 | BrowserModule, 119 | InputTextareaModule, 120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | 128 | 129 | ## Then back fill with a rich text editor 130 | 131 | 132 | npm install quill --save 133 | 134 | Update your CLI with the scripts & styles you need: 135 | 136 | 137 | "styles": [ 138 | "styles.css", 139 | "../node_modules/roboto-fontface/css/roboto/roboto-fontface.css", 140 | "../node_modules/font-awesome/css/font-awesome.css", 141 | "../node_modules/primeng/resources/primeng.css", 142 | "../node_modules/primeng/resources/themes/bootstrap/theme.css", 143 | "../node_modules/quill/dist/quill.core.css", 144 | "../node_modules/quill/dist/quill.snow.css" 145 | ], 146 | "scripts": [ 147 | "../node_modules/chart.js/dist/Chart.js", 148 | "../node_modules/quill/dist/quill.js" 149 | ], 150 | 151 | Added the editor module to your module loader: 152 | 153 | EditorModule 154 | 155 | Then you're good to go: 156 | 157 | 158 | 159 | Don't forget to style the size. 160 | 161 | ## Adding a Calendar 162 | 163 | First, add the module: 164 | 165 | CalendarModule 166 | 167 | Then add the backing property: 168 | 169 | startDate: [new Date(), Validators.required], 170 | 171 | Then the markup: Date format is US by default so set you prefs with the dateFormat attribute 172 | 173 | 174 | 175 |
176 | 177 | 178 | 179 |
180 | 181 | Reader Exercise: Set a [minDate] property to make sure it's greater than this current year. 182 | 183 | [minDate]="minProjectDate" 184 | 185 | private minProjectDate = new Date(); 186 | 187 | 188 | ## Calendar Options to Explore 189 | 190 | Staggering array of customisation is supported - min and max dates, steps between days, multimonth display, time selection, formatting, the list goes on. See docs. 191 | 192 | Basic Support for i18n through supplying locale arrays for your own day, month names,etc. 193 | 194 | Reader Exercise: Hook into the Calendar select by implementing an `(onSelect)` and display an alert of the date. 195 | 196 | 197 | ## Implementing Radios 198 | 199 | First, add the module: 200 | 201 | RadioButtonModule 202 | 203 | Then code up some buttons 204 | 205 | 206 | 207 | 208 | 209 | And bind to a backing property with a default value: 210 | 211 | projectType: ['B'], 212 | 213 | And some styling please: 214 | 215 | p-radioButton { 216 | display:block; 217 | margin: .7em; 218 | } 219 | 220 | Exercise: Refactor with an *ngFor to use a backing property. 221 | 222 | ## Wrap it in a fieldset 223 | 224 | First, add the module: 225 | 226 | FieldsetModule 227 | 228 | Then add the markup around the radios: 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | The labels are clickable as well. 237 | 238 | They can be toggled, and collapsible to: 239 | 240 | 241 | [toggleable]="true" [collapsed]="false" 242 | 243 | Trap: collapsed="false" (without the square brackets) won't work since you're binding the string "false" which is a true non-zero string in JavaScript. Be warned - if false isn't working, that's why! 244 | 245 | ## Dropdowns with a single developer 246 | 247 | If you only need to select a single value, drop downs are the win: 248 | 249 | DropdownModule 250 | 251 | 252 | Then we need our backing data object to actually pick from: 253 | 254 | private allDevs =[ 255 | 256 | { label: 'Jill', value: 'Jill Cool'}, 257 | { label: 'Joe', value: 'Joe Cool'}, 258 | { label: 'Mary', value: 'Mary Cool'}, 259 | 260 | ] 261 | 262 | And our form control object property): 263 | 264 | selectedDevs: [''] 265 | 266 | 267 | Then the markup 268 | 269 | 270 | 271 | ## Getting Staff - Multiselect module 272 | 273 | First, add the module: 274 | 275 | MultiSelectModule 276 | 277 | 278 | And our form control object property (note the array for multiselect): 279 | 280 | selectedDevs: [[]] 281 | 282 | Finally, we need our markup: 283 | 284 | 285 | 286 | 287 | Notice the default label. 288 | 289 | 290 | ## Getting Staff - A Listbox approach 291 | 292 | First, add the module: 293 | 294 | ListboxModule 295 | 296 | Then restyle: 297 | 298 | 300 | 301 | 302 | Working with Custom Templates: 303 | 304 | Note: This applies to drop downs, multiselects, and many of the components we've already covered. 305 | 306 | ("item" tells Prime to use custom templates. Let is used for the assignment to that in *ngFor style) 307 | 308 | 309 | 310 | 317 | 318 | 319 | And add some styling: 320 | 321 | p-listbox /deep/ .ui-listbox { 322 | width: 100%; 323 | height: 300px; 324 | } 325 | 326 | .avatar { 327 | float: left; 328 | margin: 5px; 329 | height: 50px; 330 | } 331 | 332 | .devName { 333 | font-size: large; 334 | display:inline-block; 335 | margin:15px 10px 0 10px; 336 | min-height: 50px; 337 | } 338 | 339 | Add filtering if you like: 340 | 341 | [filter]="true" 342 | 343 | Reader Exercise: bind to the filter value using the filterValue attribute. 344 | 345 | 346 | ## Let's rate our module with spinners 347 | 348 | Import the module: 349 | 350 | SpinnerModule 351 | 352 | Then add to the backing property: 353 | 354 | rating: [3] 355 | 356 | Then introduce the markup: 357 | 358 | 359 | 360 | Demonstrate min and max working by typing in bigger numbers then rounding down. 361 | 362 | ## Or sliders if you like: 363 | 364 | Import the module: 365 | 366 | SliderModule 367 | 368 | Bind to the same property, setting max and min as appropriate: 369 | 370 |
{{ projectForm.getRawValue() | json }}
371 | 372 | 373 | 374 | ## Or a propery star rating components 375 | 376 | First, add the module: 377 | 378 | RatingModule 379 | 380 | 381 | Then implement the markup: 382 | 383 | 384 | 385 | 386 | Or remove the cancel... 387 | 388 | [cancel]=false 389 | 390 | -------------------------------------------------------------------------------- /xample-scripts/4.grids.md: -------------------------------------------------------------------------------- 1 | 2 | # Grids 3 | 4 | ## Getting Setup 5 | 6 | First, add the module: 7 | 8 | DataTableModule 9 | 10 | Generate a alltimes component: 11 | 12 | ng g c alltimes 13 | 14 | Add it to our menu (if we have introduced that yet): 15 | 16 | {label: 'All Timesheets', icon: 'fa-calendar', routerLink: ['/alltimes'], command: (event) => handleSelected(event)}, 17 | 18 | 19 | ## Table Basics 20 | 21 | ### Skeleton Markup 22 | 23 | Let's create some data to render: 24 | 25 | 26 | private allTimesheetData = [ 27 | 28 | { user: 'Glen', project: 'Payroll App', category: 'Backend', startTime: 1000, endTime: 1700, date: 1434243 }, 29 | { user: 'Karen', project: 'Agile Times', category: 'Frontend', startTime: 900, endTime: 1700, date: 1434243 }, 30 | { user: 'Si', project: 'Mobile App', category: 'Operations', startTime: 1100, endTime: 1700, date: 1434243 }, 31 | { user: 'Rohit', project: 'Agile Times', category: 'Backend', startTime: 800, endTime: 1700, date: 1434243 }, 32 | 33 | ]; 34 | 35 | 36 | Then let's the most basic tabular markup, copying properties to friendly name: 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ### Zebra / Column Layout / Responsive 48 | 49 | ### Sorting 50 | 51 | Making a column sortable, is just adding an attribute: 52 | 53 | 54 | 55 | 56 | You can customise the inital sort using p-dataTable tags (where 1 is ascending, and -1 is descending). Note the arrows are handled for you.: 57 | 58 | sortField="startTime" [sortOrder]="1" 59 | 60 | Not the attribute settings here. Some are bracketed. 61 | 62 | **Note:** Couldn't get multi column sort to work 63 | 64 | You can sort on multilple columns by configuring the datatable itself: 65 | 66 | [sortMode]="multiple" 67 | 68 | And add a sortable to our category: 69 | 70 | 71 | 72 | And you'll need to hold down the meta key (alt?) to get this going. 73 | 74 | Challenge: And there is room to go custom on the sort routine as well by setting `sortable="custom"` implmenting an `(sortFunction)="mysort($event)` routine. Try it now! 75 | 76 | ### Filtering 77 | 78 | Imaging we want to filter on project: 79 | 80 | 81 | 82 | Notice if we search for "app" we get no hits, let's customise the message: 83 | 84 | emptyMessage="No results right now" 85 | 86 | Now let's actually change how the filter matches. 87 | 88 | You can have a filter match mode: 89 | 90 | filterMatchMode="contains" 91 | 92 | startsWith 93 | endsWith 94 | contains 95 | equals 96 | in 97 | 98 | Let's implement the "contains" filter: 99 | 100 | 101 | 102 | Notice that if I sort on start time DESC, then filter on project, the filtering is maintained. With nothing for you to do! 103 | 104 | ### All-in custom filtering 105 | 106 | Mark the datatable with a local variable: 107 | 108 | #dt 109 | 110 | Implement the custom filter: 111 | 112 | 115 | 116 | Construct the backing object: 117 | 118 | private allProjects = ['', 'Payroll App', 'Mobile App', 'Agile Times']; 119 | 120 | Alas. Actually, a dropdown needs a ` { lable: "my label", value: 'myvalue' }` 121 | 122 | private allProjects = ['', 'Payroll App', 'Mobile App', 'Agile Times'].map ( (proj) => { return { label: proj, value: proj }}); 123 | 124 | 125 | 126 | 127 | ### Reorder & Resize 128 | 129 | Need to allow users to re-order columns: 130 | 131 | [reorderableColumns]="true" 132 | 133 | If they need to resize them: 134 | 135 | [resizableColumns]="true" columnResizeMode="fit" 136 | 137 | * fit = keep the table the same width, just resize adjacent column inverse to your changes 138 | * expand = expand/shrink the table width on resize 139 | 140 | Challenge: how would you preserve width/ordering? 141 | 142 | ### Facets: Custom Headers and Footers & Global Filtering 143 | 144 | Totals for our times? ColGroups 145 | 146 | How about some global filtering? 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | And maybe some styling: 157 | 158 | p-dataTable /deep/ .ui-datatable-footer input { 159 | margin-left: 0.5em; 160 | font-size: larger; 161 | padding: 3px; 162 | } 163 | 164 | 165 | ### Exporting 166 | 167 | Add some markup to the footer of the table: 168 | 169 | 170 | 171 | You can customise the filename by a setting on the datatable: 172 | 173 | exportFilename="users" 174 | 175 | And a little styling: 176 | 177 | p-dataTable /deep/ .ui-datatable-footer { 178 | min-height: 60px; 179 | } 180 | 181 | 182 | ## Table Editing 183 | 184 | ### Inplace Editing (and Column Templating using the body template) 185 | 186 | **Note:** You have to mark **both** the datatable, and the individual column as editable. 187 | 188 | 189 | 190 | A common case is the incell editing of text fields. 191 | 192 | 193 | 194 | And suddenly you're editing! 195 | 196 | What if you want to use custom controls (date picker or drop down for example)? Again, well supported. 197 | 198 | Then take advantage of the "editor" template, just as we previously took advantage of the filter template. 199 | 200 | 203 | 204 | You can now edit the project! If you don't want to use two way binding, you are welcome to use a callback `onEditComplete` or `onEditCancel`. 205 | 206 | **Note:** that you need to add the event handler to the dataTable, not the column. 207 | 208 | (onEditComplete)="onEditComplete($event) 209 | 210 | In the payload, you get some interesting values: 211 | 212 | event.column.field = The field name that was edited on your domain object 213 | event.data = the entire object used to render the row (with the updated value) 214 | 215 | Let's take advantage of that info to render an alert with what's actually changed (you could use this hook to update a backend database or whatever you like). 216 | 217 | onEditComplete(editInfo) { 218 | alert(`You edited the ${editInfo.column.field} field. The new value is ${editInfo.data[editInfo.column.field]}`); 219 | } 220 | 221 | 222 | ### Row Selection (single) 223 | 224 | You can have the datatable track row selection in either singles or multiples. Let's start with singles: 225 | 226 | selectionMode="single" [(selection)]="selectedTime" 227 | 228 | 229 | And put in a backing property to catch the object that backs the row: 230 | 231 | private selectedTime : any; 232 | 233 | 234 | If you need to event handle, that's on the cards too: 235 | 236 | (onRowSelect)="onRowSelect($event)" 237 | 238 | You can use the data field to access the entire row that was clicked on, or just use the databound property `selectedTime`: 239 | 240 | onRowSelect(rowInfo) { 241 | console.log(JSON.stringify(rowInfo.data)); 242 | console.log(JSON.stringify(this.selectedTime)); 243 | } 244 | 245 | ### Row Selection (multiple) 246 | 247 | Going multiple just means changing the markup: 248 | 249 | 250 | selectionMode="multiple" [(selection)]="selectedTimes" (onRowSelect)="onRowSelect($event)"> 251 | 252 | 253 | And the backing property: 254 | 255 | private selectedTimes : Array; 256 | 257 | And then using the Control key to multi-select. 258 | 259 | In this case your rowInfo.data in the callback will be the latest row clicked on, and your bound `selectedTimes` property will have the full array. 260 | 261 | Although it's cooler to go Checkbox: 262 | 263 | 264 | 265 | **Note:** You should now remove the "selection" property on the p-dataTable element. Otherwise you won't be able to use the edit property later since the row selection will catch the click. 266 | 267 | Kinda nice, but the massive column is a pain, let's give it some style 268 | 269 | ### Styleclass 270 | 271 | Giving it some style, let's us share a trick for prime components: 272 | 273 | 274 | 275 | Then you get all select for free. 276 | 277 | p-dataTable /deep/ .selectBoxColumn { 278 | width: 43px; 279 | } 280 | 281 | ### Context Menu 282 | 283 | First, you'll need to drag in the module: 284 | 285 | ContextMenuModule 286 | 287 | Define a backing ContextMenu: 288 | 289 | private contextMenu: MenuItem[]; 290 | 291 | We'll use ngOnInit to setup the items we need, and a callback for when they are invoked: 292 | 293 | ngOnInit() { 294 | this.contextMenu = [ 295 | {label: 'Debug', icon: 'fa-bug', command: (event) => this.onDebug(this.selectedTimes)}, 296 | {label: 'Delete', icon: 'fa-close', command: (event) => this.onDelete(this.selectedTimes)} 297 | ]; 298 | } 299 | 300 | We'll want to implement those callbacks: (**Note:** The selectedTimes will **always** be an array of selected items, not the onRowSelect) 301 | 302 | onDebug(selectedTimes : any) { 303 | console.log(JSON.stringify(selectedTimes)); 304 | } 305 | 306 | onDelete(selectedTimes : any) { 307 | this.allTimesheetData = this.allTimesheetData.filter( (row) => { 308 | return !selectedTimes.includes(row); 309 | }) 310 | } 311 | 312 | Then create the context menu, and a matching local Variable to bind to: 313 | 314 | 315 | 316 | And finally, link the context menu to the actual data table: 317 | 318 | [contextMenu]="tableContextMenu"> 319 | 320 | I've added some styling, since there's too much blue going on.. 321 | 322 | p-contextMenu /deep/ .ui-menuitem-active a { 323 | background-color: #F15B2A !important; 324 | } 325 | 326 | This is going to be handy for CRUD dialog operations (when we cover it next module) 327 | 328 | ### Crud Operations 329 | 330 | This needs to be done once we have introduced Dialogs in the next chapter. Keep a placeholder here to remind yourself. 331 | 332 | ## Tables at Scale 333 | 334 | ### Dynamic Columns 335 | 336 | Maybe for the import of a CSV file? 337 | 338 | ### Row Groups 339 | 340 | Group by Project 341 | 342 | ### Normal Scrolling 343 | 344 | First, let's beef up our data... 345 | 346 | for(let x=0; x < 5; x++) { 347 | this.allTimesheetData = this.allTimesheetData.concat(this.allTimesheetData); 348 | } 349 | 350 | And that looks pretty shabby. One option is to simply have a fixed height scrollable: 351 | 352 | scrollable="true" scrollHeight="270px" 353 | 354 | Which looks better, and our header and footer are frozen. 355 | 356 | But what if our dataset is *much* bigger? Say 100k records? 357 | 358 | ### Virtual Scrolling 359 | 360 | Virtual Scrolling takes advantage of a feature called LazyLoading built into the grid. 361 | 362 | The same mechanism is used for paginating through large datasets with a paginator. So I'm going to show you that instead. 363 | 364 | Challenge: After doing the module on LazyLoading, come back and implenet that virtual scroller. 365 | 366 | ### Database of users 367 | 368 | We'll need a large database to make this matter. Doing things in memory won't demonstrate a typical flow, so I'm going to use IndexedDb built into yoru browser and the Dexie library to access it. Dexie follows a query flow that is familar for more ORM-style systems, so it's a lightweight example without external setup. 369 | 370 | We need a few million records to pull this off. 371 | 372 | npm install --save dexie 373 | npm install --save-dev @types/dexie 374 | 375 | 376 | I've created a button to populate our db: 377 | 378 |
Count: {{ recordCount }}
379 | 380 | 381 | 382 | 383 | And some backend code to do the work: 384 | 385 | private db: Dexie; 386 | 387 | constructor() { 388 | 389 | this.db = new Dexie('AgileTimes'); 390 | 391 | // Define a schema 392 | this.db.version(1).stores({ 393 | timesheet: 'id,user,project,category,startTime,endTime,date' 394 | }); 395 | 396 | 397 | } 398 | 399 | ### Pagination and Lazy Loading 400 | 401 | [lazy]="true" (onLazyLoad)="loadTimes($event)" 402 | 403 | 404 | Adding pagination involves a few extra steps 405 | 406 | [paginator]="true" [pageLinks]="5" [rowsPerPageOptions]="[5,10,20,50,1000]" [rows]="5" [totalRecords]="recordCount" 407 | 408 | private recordCount: number = 5; 409 | 410 | constructor() { 411 | ... 412 | this.getRecordCount().then( (count) => { this.recordCount = count }); 413 | 414 | } 415 | 416 | Implementing Load Times: 417 | 418 | 419 | event.rows = max number of rows to return to fill the table (might be null initially) 420 | event.first = offset into the table 421 | event.sortField = field to sort on 422 | event.filters = { 423 | "project": { value: "Payroll App", matchMode: "equals" } 424 | 425 | } 426 | event.globalFilter = "my filter term" 427 | 428 | We can use those values to build up a basic query: 429 | 430 | 431 | loadTimes(event: LazyLoadEvent) { 432 | 433 | let table = this.db.table("timesheet"); 434 | 435 | var query : any; 436 | 437 | // Dexie doesn't support ordering AND filtering, so we branch here 438 | if (event.filters && event.filters["project"]) { 439 | query = table.where("project").equals(event.filters["project"]["value"]); 440 | } else { 441 | query = table.orderBy(event.sortField); 442 | } 443 | 444 | query = query 445 | .offset(event.first) 446 | .limit(event.rows); 447 | 448 | if (event.sortOrder == -1) { 449 | query = query.reverse(); 450 | }; 451 | 452 | query.toArray( (nextBlockOfTimes) => { 453 | this.allTimesheetData = nextBlockOfTimes; 454 | }); 455 | } 456 | 457 | Demonstrate how we can now filter. And sort. But not at the same time! 458 | 459 | **Challenge:**: Go back and implement the delete and debug and "click to edit" functions. 460 | 461 | **Challenge:** I haven't used the globalFilter here - I would just need to query all the fields I care about searching. It wouldn't be hard to implement by a chain of where clauses with contains clauses. Feel free to have a crack. 462 | -------------------------------------------------------------------------------- /xample-scripts/5.tabs.md: -------------------------------------------------------------------------------- 1 | # Navigation 2 | 3 | 4 | ## Tabs 5 | 6 | ### Styling our Timesheet with Tabs 7 | 8 | First, add the module: 9 | 10 | TabViewModule 11 | 12 | I've installed moment to work with dates 13 | 14 | npm install moment --save 15 | 16 | Then update angular-cli.json 17 | 18 | "scripts": [ 19 | "../node_modules/chart.js/dist/Chart.js", 20 | "../node_modules/quill/dist/quill.js", 21 | "../node_modules/moment/min/moment.min.js" 22 | ], 23 | 24 | 25 | 26 | ### Moving to Weekdays 27 | 28 | Imagine We have some timesheeting data for our user: 29 | 30 | private userTimeData = [ 31 | 32 | { day: "Monday", startTime: '9:00', endTime: '17:00', project: 'Agile Times', category: "Frontend" }, 33 | { day: "Tuesday", startTime: '9:00', endTime: '17:00', project: 'Payroll App', category: "Backend" }, 34 | { day: "Wednesday", startTime: '9:00', endTime: '17:00', project: 'Point of Sale App', category: "Operations" }, 35 | { day: "Thursday", startTime: '9:00', endTime: '17:00', project: 'Mobile App', category: "Planning" }, 36 | { day: "Friday", startTime: '9:00', endTime: '17:00', project: 'Agile Times', category: "Requirements" }, 37 | 38 | ] 39 | 40 | Fun with TabView 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | But in our case, we really want dynamic views! 56 | 57 | This is just as common in a biz app - you want tabs for clients, etc. 58 | 59 | 60 | ### Dynamic Views 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ### Handling Change Events 76 | 77 | You can hook into tab changes through the (onChange) handler on the tabView container. 78 | 79 | Note: it's on the container (tabView), not the tabPanel elements 80 | 81 | 82 | 83 | This gives you an object with an index of the selected tab. 84 | 85 | If you're using dynamic tabs, you'll have some math to do. Enjoy! 86 | 87 | changeTabs(event) { 88 | let index = event.index; 89 | let dayOfWeek = this.daysOfWeek[index]; 90 | let selectedDay = moment().day(dayOfWeek); 91 | this.day = selectedDay.format("dddd"); 92 | this.dateAndMonth = selectedDay.format("MMMM Do, YYYY"); 93 | } 94 | 95 | ## Diving Into Dialogs (Everyday Stuff) 96 | 97 | ### Creating Your First Dialog 98 | 99 | You probably wouldn't use a dialog for this, too much user friction - just create it in place per our datatables... but it gives us an excuse to go berserk with controls. 100 | 101 | First we'll need our magic module: 102 | 103 | 104 | import {DialogModule} from 'primeng/primeng'; 105 | 106 | Then our markup - we're going to create a modal dialog (default is non modal); 107 | 108 | 109 | Our Time Entry editor goes here. 110 | 111 | 112 | 113 | 114 | And the backing code: 115 | 116 | private displayEditDialog = false; 117 | 118 | Let's create a button to launch it... 119 | 120 | 121 | 122 | And let's actually display this dialog: 123 | 124 | addNewEntry() { 125 | this.displayEditDialog = true; 126 | } 127 | 128 | Looking cool. I want to give it a bit more structure: 129 | 130 | ### Dialog Footers (and headers) 131 | 132 | Like datatable, we can customise the headers and footers. Note the use of secondary class buttons for cancel. 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | There is also a matching p-header if you need it.. And style them if needed: 141 | 142 | p-footer button { 143 | float: right; 144 | margin: 0.5em; 145 | } 146 | 147 | 148 | 149 | 150 | ### Adding Steps 151 | 152 | 153 | First we'll need our magic module: 154 | 155 | 156 | import {StepsModule} from 'primeng/primeng'; 157 | 158 | Let's add some markup: 159 | 160 | 161 | 162 | And some styling to fill the dialog. Since I have four steps, fill them 25% of the dialog. 163 | 164 | p-steps /deep/ .ui-steps-item { 165 | width: 25%; 166 | } 167 | 168 | Now we'll supply the menu components to show the pages: 169 | 170 | private dialogPages: MenuItem[] = [ 171 | {label: "Time"}, 172 | {label: "Project"}, 173 | {label: "Place"}, 174 | {label: "People"} 175 | ]; 176 | 177 | And we have our steps! But now let's display some lightweight conditional markup. 178 | 179 | TWo approaches. Use the callback from the MenuItem, or just use an ngIf, with some enums for good measure. 180 | 181 | Add a switch to the p-dialog 182 | 183 | [ngSwitch]="dialogPageIndex" 184 | 185 | Then use some markup to switch on an enum 186 | 187 |
188 | Our time page goes here. 189 |
190 |
191 | Our project page goes here. 192 |
193 |
194 | Our place page goes here. 195 |
196 |
197 | Our people page goes here. 198 |
199 | 200 | We do have to workaround enum scope issues... Put this above the class 201 | 202 | export enum PageNames { 203 | TimePage, 204 | ProjectPage, 205 | PlacePage, 206 | PeoplePage 207 | } 208 | 209 | And this inside it: 210 | 211 | // Need to import the enum to reference in the view 212 | private PageNames = PageNames; 213 | 214 | private dialogPageIndex : PageNames = PageNames.TimePage; 215 | 216 | 217 | Now we can navigate between divs. 218 | 219 | 220 | 221 | 222 | ### Schedule 223 | 224 | Based on [Full Calendar](https://fullcalendar.io/) - a popular open source JS calendar. 225 | 226 | First import the module: 227 | 228 | import {ScheduleModule} from 'primeng/primeng'; 229 | 230 | 231 | Add the fullcalendar dep: 232 | 233 | npm install fullcalendar --save 234 | npm install jquery --save 235 | npm install moment --save // if you haven't already 236 | 237 | And add it to your scripts loader in .angular-cli.json: 238 | 239 | "styles": [ 240 | ... 241 | "../node_modules/fullcalendar/dist/fullcalendar.css" 242 | ], 243 | 244 | 245 | "scripts": [ 246 | ... 247 | "../node_modules/moment/min/moment.min.js", 248 | "../node_modules/jquery/dist/jquery.js", 249 | "../node_modules/fullcalendar/dist/fullcalendar.js" 250 | ], 251 | 252 | And restart. 253 | 254 | First the markup: 255 | 256 | 258 | 259 | Then the header config: 260 | 261 | private headerConfig = { 262 | left: 'prev,next today', 263 | center: 'title', 264 | right: 'month,agendaWeek,agendaDay' 265 | }; 266 | 267 | Let's get started with a simple event: 268 | 269 | private events = [ 270 | { 271 | title: 'Recent Work', 272 | start: '2017-04-06 07:00:00', 273 | end: '2017-04-06 08:00:00' 274 | } 275 | 276 | ### Trees 277 | 278 | Import the module: 279 | 280 | import {TreeModule,TreeNode} from 'primeng/primeng'; 281 | 282 | Write some markup: 283 | 284 |
285 | 286 |
287 | 288 | projectTree is a TreeNode[]. I've got some sample data in a file called `sample.projects.data.ts`: 289 | 290 | private projectsTree : TreeNode[] = SampleProjectsData.projects; 291 | 292 | private selectedProject : TreeNode; 293 | 294 | No doubt, you're curious on the TreeNode... 295 | 296 | export interface TreeNode { 297 | label?: string; 298 | data?: any; 299 | icon?: any; 300 | expandedIcon?: any; 301 | collapsedIcon?: any; 302 | children?: TreeNode[]; 303 | leaf?: boolean; 304 | expanded?: boolean; 305 | type?: string; 306 | parent?: TreeNode; 307 | partialSelected?: boolean; 308 | styleClass?: string; 309 | draggable?: boolean; 310 | droppable?: boolean; 311 | selectable?: boolean; 312 | } 313 | 314 | They have a `label` and `icon` (which can be expanded or collapsed) - which are both shown on the screen. They have a `data` which you can bundle in your own data if you need it. And `children` is used to hold child nodes (with their own labels and icons) 315 | 316 | Here's an extract from our sample data file. 317 | 318 | 319 | { 320 | "label": "Projects", 321 | "data": "Documents Folder", 322 | "expandedIcon": "fa-folder-open", 323 | "collapsedIcon": "fa-folder", 324 | "children": [{ 325 | "label": "Agile Times", 326 | "data": "agile", 327 | "expandedIcon": "fa-folder-open", 328 | "collapsedIcon": "fa-folder", 329 | "children": [ 330 | {"label": "Frontend", "icon": "fa-chrome", "data": "fe"}, 331 | {"label": "Backend", "icon": "fa-cloud", "data": "be"}, 332 | {"label": "Operations", "icon": "fa-cogs", "data": "ops"} 333 | ] 334 | }, 335 | 336 | And here's the tree in operation. 337 | 338 | Alas! We can select the root nodes. 339 | 340 | Let's make sure only the most leaf nodes are selectable: 341 | 342 | "selectable": false, 343 | 344 | If you want a more traditional tree, remove the horizontal layout! 345 | 346 | 347 | 348 | 349 | ### Adding a Map 350 | 351 | Add the module: 352 | 353 | import {GMapModule} from 'primeng/primeng'; 354 | 355 | Add your [google api key](https://developers.google.com/maps/documentation/javascript/get-api-key), to your index.html loader: 356 | 357 | Import the JavaScript for Google Maps API (in the HEAD of your index.html): 358 | 359 | 360 | 361 | 362 | Add some markup to position the map: 363 | 364 |
365 | 367 |
368 | 369 | *Note:* You must style up a height or it won't show up: 370 | 371 | p-gmap /deep/ .gmap { 372 | width:100%; 373 | height: 320px; 374 | } 375 | 376 | Provide the overlays and initial position: 377 | 378 | 379 | declare var google: any; 380 | 381 | ngOnInit() { 382 | 383 | this.mapOptions = { 384 | 385 | center: {lat: -33.8688, lng: 151.2093}, 386 | zoom: 5 387 | }; 388 | 389 | // http://www.mapcoordinates.net/en 390 | this.mapOverlays = [ 391 | new google.maps.Marker({position: {lat: -35.3075, lng: 149.124417}, title: "Canberra Office"}), 392 | new google.maps.Marker({position: {lat: -33.8688, lng: 151.2093}, title: "Sydney Office"}), 393 | new google.maps.Marker({position: {lat: -37.813611, lng: 144.963056}, title: "Melbourne Office"}), 394 | new google.maps.Marker({position: {lat: -28.016667, lng: 153.4}, title: "Gold Coast Office"}) 395 | ]; 396 | 397 | } 398 | 399 | Catch the click on the markers: 400 | 401 | (onOverlayClick)="onMarkerClick($event)" 402 | 403 | Implement the click: 404 | 405 | onMarkerClick(markerEvent) { 406 | console.log(markerEvent); 407 | console.log(`You clicked on ${markerEvent.overlay.title} at ${markerEvent.overlay.position}`); 408 | 409 | markerEvent.map.panTo(markerEvent.overlay.position); 410 | markerEvent.map.setZoom(12); 411 | } 412 | 413 | You are given a handle to: 414 | 415 | markerEvent.overlay (the one clicked on) which has a title and .position() 416 | markerEvent.map (the Google Map object they clicked on) 417 | 418 | 419 | ### Adding a datagrid 420 | 421 | Pull in the module: 422 | 423 | import {DataGridModule} from 'primeng/primeng'; 424 | 425 | Generate some [random data](https://www.mockaroo.com/). 426 | 427 | concat("http://i.pravatar.cc/100?u=", firstName) 428 | 429 | 430 | Import that random data: 431 | 432 | 433 | private people = PeopleData.people; 434 | 435 | Add the markup to paginate it (note that "rows" is individual squares visible) 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | Kinda fun by the styling needs work: 446 | 447 | p-dataGrid /deep/ .ui-panel { 448 | border: 0px; 449 | } 450 | p-dataGrid /deep/ .ui-panel-titlebar { 451 | font-size: smaller; 452 | background-color: #F15B2A !important; 453 | text-align: center; 454 | height: 35px; 455 | } 456 | 457 | 458 | p-dataGrid /deep/ .ui-panel-content { 459 | padding: 0px; 460 | } 461 | 462 | p-dataGrid /deep/ .ui-panel-content img { 463 | display: block; 464 | margin-left: auto; 465 | margin-right: auto; 466 | width: 150px; 467 | } 468 | 469 | Much better! 470 | 471 | You can use lazy loading as discussed in the grids module to take things further! 472 | 473 | ### Tooltips 474 | 475 | First import the module: 476 | 477 | import {TooltipModule} from 'primeng/primeng'; 478 | 479 | Then add the markup.. 480 | 481 | 482 | 483 | Position is options. 484 | 485 | 486 | ### Confirmation Dialogs & Dialog Events 487 | 488 | You sometimes want to catch the cancel (if form is dirty), and confirm that's what they want. 489 | 490 | Requires both a service and a module to import: 491 | 492 | import {ConfirmDialogModule,ConfirmationService} from 'primeng/primeng'; 493 | 494 | providers: [ DataService, ConfirmationService ], 495 | 496 | Update the dialog to catch the hide: (also an (onShow) which can be helpful for intializing dialog elements) 497 | 498 | . 499 | 500 | 501 | And update the button: 502 | 503 | 504 | 505 | Add our confirm dialog: 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | We'll need to inject the confirmation service in backing code: 515 | 516 | constructor(private confirmationService: ConfirmationService) { 517 | 518 | Then implement our close operation: 519 | 520 | 521 | 522 | ### Growling 523 | 524 | First drag in the module: 525 | 526 | import {GrowlModule} from 'primeng/primeng'; 527 | 528 | Add some markup to our page: 529 | 530 | 531 | 532 | ANd let's implemeting the save of new entries: 533 | 534 | saveNewEntry() { 535 | this.displayEditDialog = false; 536 | this.messages.push({severity:'success', summary:'Entry Created', detail:'Your entry has been created'}); 537 | } 538 | 539 | Finally, let's revisit our dialog cancel operations to get rid of those console messages, and give some real feedback: 540 | 541 | cancelDialog() { 542 | this.confirmationService.confirm({ 543 | message: 'Cancel all changes. Are you sure?', 544 | accept: () => { 545 | this.displayEditDialog = false; 546 | this.messages.push({severity:'info', summary:'Edits Cancelled', detail:'No changes were saved'}); 547 | }, 548 | reject: () => { 549 | this.messages.push({severity:'warn', summary:'Cancelled the Cancel', detail:'Please continue your editing'}); 550 | } 551 | }); 552 | } 553 | 554 | Of course, you can customise how long the growls stick around, 555 | 556 | 557 | 558 | or whether you need auto disappear at all: 559 | 560 | 561 | -------------------------------------------------------------------------------- /xample-scripts/6.advanced.md: -------------------------------------------------------------------------------- 1 | 2 | # Advanced Topics 3 | 4 | 5 | ## Non-visual Components: Drag and Drop 6 | 7 | First import the module: 8 | 9 | import {DragDropModule} from 'primeng/primeng'; 10 | 11 | Make the panel droppable by adding a pDroppable label (eg `pic` in my place - I'll tag the pDraggable to match). Also add an event handler: 12 | 13 | 14 | 15 | Conditionally display the dropped image (if we have one): 16 | 17 | 18 | 19 | 20 | Drop Your Image Here 21 | 22 | 23 | But how to select the image? 24 | 25 | ## Using the Galleria 26 | 27 | Let's create a galleria with some backing images. 28 | 29 | 32 | 33 | It will need some backing image. Let's mock some up. We'll need a src, a title, and an alt. 34 | 35 | private images = [ 36 | {source: "http://i.pravatar.cc/300?u=Anne", title: "Anne"}, 37 | {source: "http://i.pravatar.cc/300?u=Kerri", alt: "Profile Pic 2", title: "Kerri"}, 38 | {source: "http://i.pravatar.cc/300?u=Mary", alt: "Profile Pic 3", title: "Mary"}, 39 | {source: "http://i.pravatar.cc/300?u=Nancy", alt: "Profile Pic 4", title: "Nancy"}, 40 | {source: "http://i.pravatar.cc/300?u=Peta", alt: "Profile Pic 5", title: "Peta"}, 41 | ] 42 | 43 | 44 | We'll mark the galleria draggable: 45 | 46 | pDraggable="pic" 47 | 48 | But now the whole thing is draggable. We just want the current image draggable: 49 | 50 | dragHandle=".ui-panel-images" 51 | 52 | That looks great. But we need be able to find the image the user clicks on. That's available in an event called: 53 | 54 | (onImageClicked)="onImageSelected($event)" 55 | 56 | But it fires too late for our drag and drop! 57 | 58 | We'll listen in on onDragStart, but pass it a handle to the galleria component. There must be some way to find out the active image to display from there. 59 | 60 | 62 | 63 | ## Inside the Black Box: Diving into PrimeNG SRC 64 | 65 | Let's side track into the [source](https://github.com/primefaces/primeng). 66 | 67 | First. Don't be intimidated! Just find the Git label for the release you're using, and you're underway. 68 | 69 | Now, look the @Input properties. 70 | 71 | And the @Output event emitters. 72 | 73 | And the template names can be handy too. 74 | 75 | These guys tell you everything that's public for setting and listening. 76 | 77 | activeIndex looks promising! But it never gets checked after startup - and it's then an interval that's firing. 78 | 79 | But I notice below that a stopSlideshow() method. That's just what I need! 80 | 81 | But how to call it? 82 | 83 | Just use a local variable to the galleria (or a @ViewChild if you prefer). 84 | 85 | 86 | 88 | 89 | Then we can implement our onDragStart() to stop the slideshow on dragging: 90 | 91 | onDragStart(galleria) { 92 | console.log(galleria); 93 | this.selectedProfile = this.images[galleria.activeIndex]; 94 | galleria.stopSlideshow(); 95 | } 96 | 97 | Which means that we now have the selected photo, so we can implement the drop! 98 | 99 | onPicDrop() { 100 | this.profileImage = this.selectedProfile.source; 101 | this.messages.push({ severity: "info", summary: "New Profile", detail: `Changed pic to ${this.selectedProfile.title }` }); 102 | } 103 | 104 | And we have drag and drop! 105 | 106 | ## Debugging? 107 | 108 | Show how to set breakpoints 109 | 110 | 111 | Now on to Unit Testing! 112 | 113 | ## Unit Testing PrimeNG Components 114 | 115 | Unit Testing is really beyond the brief of this particular course, but it's worth mentioning what's possible. 116 | 117 | First, configure your testbed to include all the modules you reference on your page: 118 | 119 | TestBed.configureTestingModule({ 120 | declarations: [ ProfileComponent ], 121 | imports: [ GrowlModule, GalleriaModule, DragDropModule, 122 | PanelModule, FieldsetModule, NoopAnimationsModule ] 123 | }) 124 | .compileComponents(); 125 | 126 | Demonstrate what happens if you don't - world of errors. 127 | 128 | You don't want to be testing Galleria, but you do want an object that looks like it. Jasmine gives you spys for just such occasions. You can mock out the stopSlideshow method. 129 | 130 | it('should stop the slideshow on starting drag', () => { 131 | 132 | let mockGalleria = { 133 | activeIndex: 2, 134 | stopSlideshow: createSpy('stopSlideshow') 135 | } 136 | 137 | component.onDragStart( mockGalleria ); 138 | expect(mockGalleria.stopSlideshow).toHaveBeenCalled(); 139 | 140 | }); 141 | 142 | 143 | What if we want to check the drop? We might need to change the scope of profileImage to test it. 144 | 145 | If you need to check the actual physical presence of the *ngIf'd IMG, you can search for it, but you have to make 146 | sure that run `fixture.detectChanges()` so the change cycle can actually re-check the ngIf. 147 | 148 | 149 | 150 | it('should update the image on drop', () => { 151 | 152 | let mockGalleria = { 153 | activeIndex: 2, 154 | stopSlideshow: createSpy('stopSlideshow') 155 | } 156 | 157 | component.onDragStart( mockGalleria ); 158 | component.onPicDrop(); 159 | 160 | expect(component.profileImage).toEqual("http://i.pravatar.cc/300?u=Mary"); 161 | 162 | fixture.detectChanges(); // You need to fire change detection since we changed the property. 163 | 164 | let imgElement = fixture.debugElement.query(By.css('#profilePic')).nativeElement; 165 | expect(imgElement).toBeTruthy(); 166 | 167 | }); 168 | 169 | 170 | 171 | ## Where to from here? 172 | 173 | There's still plenty of components to play with. Start a plunker and get going! 174 | 175 | And chime in on the discussion below to let me know what you're building! I love seeing hobby projects! 176 | --------------------------------------------------------------------------------