├── src ├── assets │ ├── .gitkeep │ ├── github.svg │ ├── angular-white-transparent.svg │ └── docs │ │ └── ng-rating.md ├── app │ ├── rating │ │ ├── rating-api │ │ │ ├── rating-api.component.scss │ │ │ ├── rating-api.component.html │ │ │ └── rating-api.component.ts │ │ ├── rating.component.scss │ │ ├── rating-demo │ │ │ ├── rating-demo.component.scss │ │ │ ├── rating-demo.component.html │ │ │ └── rating-demo.component.ts │ │ ├── rating.component.html │ │ ├── rating-routing.module.ts │ │ ├── rating.module.ts │ │ └── rating.component.ts │ ├── shared │ │ ├── header │ │ │ ├── header.component.scss │ │ │ ├── header.component.ts │ │ │ └── header.component.html │ │ ├── footer │ │ │ ├── footer.component.ts │ │ │ ├── footer.component.html │ │ │ └── footer.component.scss │ │ ├── card │ │ │ ├── card.component.scss │ │ │ ├── card.component.ts │ │ │ └── card.component.html │ │ └── shared.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app-routing.module.ts │ ├── app.module.ts │ └── app.component.spec.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── main.ts ├── index.html ├── test.ts ├── styles.scss └── polyfills.ts ├── projects └── d-ng-rating │ ├── src │ ├── lib │ │ ├── index.ts │ │ ├── public-api.ts │ │ ├── util │ │ │ └── key.ts │ │ ├── ng-rating.error.ts │ │ ├── ng-rating-label.directive.ts │ │ ├── ng-rating.module.ts │ │ ├── ng-rating.component.scss │ │ ├── ng-rating.component.html │ │ ├── ng-rating.md │ │ ├── ng-rating.component.ts │ │ └── ng-rating.component.spec.ts │ ├── public-api.ts │ └── test.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── karma.conf.js │ └── README.md ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── .editorconfig ├── tsconfig.spec.json ├── .browserslistrc ├── tsconfig.app.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tsconfig.json ├── .gitignore ├── LICENSE ├── karma.conf.js ├── README.md ├── tslint.json ├── package.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/rating/rating-api/rating-api.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public-api'; -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | yarn.lock 4 | dist -------------------------------------------------------------------------------- /src/app/rating/rating-api/rating-api.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkoandreev/d-ng-rating/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/rating/rating.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 2% 20%; 3 | background-color: rgba(0, 0, 0, 0.03); 4 | } 5 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of d-ng-rating 3 | */ 4 | 5 | export * from './lib/public-api'; 6 | 7 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.scss: -------------------------------------------------------------------------------- 1 | img { 2 | height: 1.7rem; 3 | width: 1.7rem; 4 | } 5 | 6 | .flex-spacer { 7 | flex-grow: 1; 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | “semi”: true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100%; 4 | flex-direction: column; 5 | 6 | .main-class { 7 | flex-grow: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/rating/rating-demo/rating-demo.component.scss: -------------------------------------------------------------------------------- 1 | .star { 2 | font-size: 1.5rem; 3 | color: #b0c4de; 4 | } 5 | 6 | .filled { 7 | cursor: pointer; 8 | color: #1e90ff; 9 | } 10 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './ng-rating.component'; 2 | export * from './ng-rating.error'; 3 | export * from './ng-rating.module'; 4 | export * from './ng-rating-label.directive'; -------------------------------------------------------------------------------- /projects/d-ng-rating/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/d-ng-rating", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/util/key.ts: -------------------------------------------------------------------------------- 1 | export enum Key { 2 | End = 'End', 3 | Home = 'Home', 4 | ArrowLeft = 'ArrowLeft', 5 | ArrowUp = 'ArrowUp', 6 | ArrowRight = 'ArrowRight', 7 | ArrowDown = 'ArrowDown', 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12.14' 4 | 5 | before_script: 6 | - npm install -g @angular/cli 7 | 8 | script: 9 | - npm run test-headless 10 | - npm run lint 11 | - npm run build d-ng-rating --prod 12 | -------------------------------------------------------------------------------- /projects/d-ng-rating/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "enableIvy": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'], 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.error.ts: -------------------------------------------------------------------------------- 1 | export const RATING_SIZE_ERROR = () => { 2 | throw Error('Rating size must be greater than zero.'); 3 | }; 4 | 5 | export const RATE_SET_ERROR = () => { 6 | throw Error('Rate definition must be greather than zero.'); 7 | }; 8 | -------------------------------------------------------------------------------- /projects/d-ng-rating/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/app/rating/rating.component.html: -------------------------------------------------------------------------------- 1 | 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, HostBinding } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'], 7 | }) 8 | export class FooterComponent { 9 | @HostBinding('style.width') width = '100%'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header', 5 | templateUrl: './header.component.html', 6 | styleUrls: ['./header.component.scss'], 7 | }) 8 | export class HeaderComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating-label.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef, HostBinding } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[ngRatingLabel], d-ng-rating-label', 5 | }) 6 | export class NgRatingLabelDirective { 7 | @HostBinding('class.d-ng-rating-label') get ngRatingLabel(): boolean { 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/rating/rating-api/rating-api.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-rating-api', 5 | templateUrl: './rating-api.component.html', 6 | styleUrls: ['./rating-api.component.scss'], 7 | }) 8 | export class RatingApiComponent { 9 | readonly docPath: string = 'assets/docs/ng-rating.md'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Angular 4 | Powered by 6 | darkica 8 |
9 |
10 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | mat-toolbar { 2 | justify-content: center; 3 | 4 | footer { 5 | display: flex; 6 | align-items: center; 7 | 8 | span { 9 | font-size: 12px; 10 | 11 | a { 12 | color: #f5f5f5; 13 | } 14 | } 15 | 16 | img { 17 | width: 50px; 18 | height: 50px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/d-ng-rating/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "ng", "camelCase"], 5 | "component-selector": [true, "element", "d-ng", "kebab-case"], 6 | "variable-name": { 7 | "options": ["ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Rating 5 | 6 | 7 | Github 8 | 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/app/shared/card/card.component.scss: -------------------------------------------------------------------------------- 1 | .docs-wrapper { 2 | min-height: 100px; 3 | } 4 | 5 | mat-card { 6 | margin-bottom: 2%; 7 | 8 | mat-card-subtitle { 9 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 10 | 11 | button { 12 | position: absolute; 13 | right: 0; 14 | visibility: hidden; 15 | } 16 | 17 | &:hover button { 18 | cursor: pointer; 19 | visibility: visible; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [], 6 | "paths": { 7 | "@d-ng-rating/dist": ["dist/d-ng-rating"], 8 | "@d-ng-rating/d-ng-rating": ["./projects/d-ng-rating/src/public-api"] 9 | } 10 | }, 11 | "files": [ 12 | "src/main.ts", 13 | "src/polyfills.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.d.ts" 17 | ], 18 | "exclude": [ 19 | "src/test.ts", 20 | "src/**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /projects/d-ng-rating/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "target": "es2015", 7 | "declaration": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test.ts", "**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-card', 5 | templateUrl: './card.component.html', 6 | styleUrls: ['./card.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class CardComponent implements OnInit { 10 | @Input() htmlCode: string; 11 | @Input() typescriptCode: string; 12 | @Input() cssCode: string; 13 | @Input() cardTitle: string; 14 | 15 | constructor() {} 16 | 17 | ngOnInit(): void {} 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: 'rating', 7 | loadChildren: () => import('./rating/rating.module').then((m) => m.RatingModule), 8 | }, 9 | { 10 | path: '', 11 | redirectTo: 'rating', 12 | pathMatch: 'full', 13 | }, 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], 18 | exports: [RouterModule], 19 | }) 20 | export class AppRoutingModule {} 21 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgRating 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgRatingComponent } from './ng-rating.component'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { NgRatingLabelDirective } from './ng-rating-label.directive'; 7 | 8 | @NgModule({ 9 | declarations: [NgRatingComponent, NgRatingLabelDirective], 10 | imports: [CommonModule, FormsModule, FontAwesomeModule], 11 | exports: [NgRatingComponent, NgRatingLabelDirective], 12 | }) 13 | export class NgRatingModule {} 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/assets/github.svg: -------------------------------------------------------------------------------- 1 | github-circle-white-transparent -------------------------------------------------------------------------------- /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/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | declare const require: any; 8 | 9 | // First, initialize the Angular testing environment. 10 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 11 | teardown: { destroyAfterEach: false }, 12 | }); 13 | // Then we find all the tests. 14 | const context = require.context('./', true, /\.spec\.ts$/); 15 | // And load the modules. 16 | context.keys().map(context); 17 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('angular-rating app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": ["node_modules/@types"], 15 | "lib": ["es2018", "dom"], 16 | "paths": { 17 | "@d-ng-rating/dist": ["dist/d-ng-rating"], 18 | "@d-ng-rating/d-ng-rating": ["./projects/d-ng-rating/src/public-api"] 19 | } 20 | }, 21 | "angularCompilerOptions": { 22 | "fullTemplateTypeCheck": true, 23 | "strictInjectionParameters": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/rating/rating-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { RatingComponent } from './rating.component'; 4 | import { RatingDemoComponent } from './rating-demo/rating-demo.component'; 5 | import { RatingApiComponent } from './rating-api/rating-api.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: RatingComponent, 11 | children: [ 12 | { path: '', redirectTo: 'examples', pathMatch: 'full' }, 13 | { path: 'examples', component: RatingDemoComponent }, 14 | { path: 'api', component: RatingApiComponent }, 15 | ], 16 | }, 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [RouterModule.forChild(routes)], 21 | exports: [RouterModule], 22 | }) 23 | export class RatingRoutingModule {} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /projects/d-ng-rating/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'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | declare const require: { 9 | context( 10 | path: string, 11 | deep?: boolean, 12 | filter?: RegExp 13 | ): { 14 | keys(): string[]; 15 | (id: string): T; 16 | }; 17 | }; 18 | 19 | // First, initialize the Angular testing environment. 20 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 21 | teardown: { destroyAfterEach: false }, 22 | }); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | body { 8 | margin: 0; 9 | font-family: Roboto, 'Helvetica Neue', sans-serif; 10 | } 11 | 12 | markdown { 13 | font-size: 1.1rem; 14 | 15 | li { 16 | padding-bottom: 8px; 17 | } 18 | 19 | table { 20 | border-collapse: collapse; 21 | border-radius: 2px; 22 | border-spacing: 0; 23 | margin: 0 0 32px; 24 | width: 100%; 25 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24), 0 0 2px rgba(0, 0, 0, 0.12); 26 | 27 | th { 28 | padding: 10px; 29 | border: 1px solid #ddd; 30 | background: #f5f5f5; 31 | } 32 | 33 | td { 34 | padding: 20px; 35 | border: 1px solid #ddd; 36 | display: table-cell; 37 | text-align: left; 38 | vertical-align: middle; 39 | border-radius: 2px; 40 | background-color: white; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { AppRoutingModule } from './app-routing.module'; 4 | import { AppComponent } from './app.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { RatingModule } from './rating/rating.module'; 7 | import { FlexLayoutModule } from '@angular/flex-layout'; 8 | import { SharedModule } from './shared/shared.module'; 9 | import { MatToolbarModule } from '@angular/material/toolbar'; 10 | import { MarkdownModule } from 'ngx-markdown'; 11 | 12 | @NgModule({ 13 | declarations: [AppComponent], 14 | imports: [ 15 | BrowserModule, 16 | SharedModule, 17 | MatToolbarModule, 18 | FlexLayoutModule, 19 | AppRoutingModule, 20 | BrowserAnimationsModule, 21 | RatingModule, 22 | MarkdownModule.forRoot(), 23 | ], 24 | bootstrap: [AppComponent], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: inline-flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | outline: none; 6 | } 7 | 8 | @mixin clear-item { 9 | border: none; 10 | outline: none; 11 | background: transparent; 12 | } 13 | 14 | .d-ng-rating { 15 | &-item { 16 | @include clear-item(); 17 | 18 | &-icon { 19 | font-size: 1.875rem; 20 | transition: 0.3s; 21 | } 22 | 23 | &-disabled { 24 | pointer-events: none; 25 | opacity: 0.7; 26 | } 27 | 28 | &-icon-hover { 29 | color: #ffd700; 30 | cursor: pointer; 31 | } 32 | } 33 | 34 | &-cancel { 35 | @include clear-item(); 36 | 37 | fa-icon { 38 | font-size: 1.875rem; 39 | color: #dc143c; 40 | 41 | &:hover { 42 | cursor: pointer; 43 | } 44 | } 45 | } 46 | 47 | &-label { 48 | display: inline-flex; 49 | align-self: center; 50 | padding-left: 1rem; 51 | font-size: 1.875rem; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /projects/d-ng-rating/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d-ng-rating", 3 | "version": "4.0.0", 4 | "author": { 5 | "name": "Darko Andreev", 6 | "email": "andreev.darko@gmail.com", 7 | "url": "https://github.com/darkoandreev" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/darkoandreev/d-ng-rating" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/darkoandreev/d-ng-rating/issues" 15 | }, 16 | "homepage": "https://darkoandreev.github.io/d-ng-rating/", 17 | "keywords": [ 18 | "angular", 19 | "typescript", 20 | "rating", 21 | "star rating", 22 | "star", 23 | "rate component", 24 | "ng rating" 25 | ], 26 | "license": "MIT", 27 | "dependencies": { 28 | "tslib": "^2.0.0" 29 | }, 30 | "peerDependencies": { 31 | "@angular/common": "^11.0.8", 32 | "@angular/core": "^11.0.8", 33 | "@fortawesome/angular-fontawesome": "^0.8.1", 34 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 35 | "@fortawesome/free-solid-svg-icons": "^5.15.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HeaderComponent } from './header/header.component'; 4 | import { FooterComponent } from './footer/footer.component'; 5 | import { MatToolbarModule } from '@angular/material/toolbar'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { CardComponent } from './card/card.component'; 9 | import { MatCardModule } from '@angular/material/card'; 10 | import { MatTabsModule } from '@angular/material/tabs'; 11 | import { ClipboardModule } from 'ngx-clipboard'; 12 | 13 | @NgModule({ 14 | declarations: [HeaderComponent, FooterComponent, CardComponent], 15 | imports: [ 16 | CommonModule, 17 | ClipboardModule, 18 | MatToolbarModule, 19 | MatIconModule, 20 | MatButtonModule, 21 | MatCardModule, 22 | MatTabsModule, 23 | ], 24 | exports: [HeaderComponent, FooterComponent, CardComponent], 25 | }) 26 | export class SharedModule {} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Darko Andreev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/app/shared/card/card.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ cardTitle }} 3 | 4 | 5 | 6 | 9 |
10 |
{{ htmlCode }}
11 |
12 |
13 | 14 | 17 |
18 |
{{ typescriptCode }}
19 |
20 |
21 | 22 | 25 |
26 |
{{ cssCode }}
27 |
28 |
29 |
30 |
31 | 32 |
33 | -------------------------------------------------------------------------------- /src/assets/angular-white-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /projects/d-ng-rating/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/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-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular9-rating'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/rating/rating.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RatingComponent } from './rating.component'; 4 | import { RatingDemoComponent } from './rating-demo/rating-demo.component'; 5 | import { RatingApiComponent } from './rating-api/rating-api.component'; 6 | import { RatingRoutingModule } from './rating-routing.module'; 7 | import { MatTabsModule } from '@angular/material/tabs'; 8 | import { MatCardModule } from '@angular/material/card'; 9 | import { NgRatingModule } from '@d-ng-rating/d-ng-rating'; 10 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 11 | import { SharedModule } from '../shared/shared.module'; 12 | import { MarkdownModule } from 'ngx-markdown'; 13 | import { HttpClientModule } from '@angular/common/http'; 14 | 15 | @NgModule({ 16 | declarations: [RatingComponent, RatingDemoComponent, RatingApiComponent], 17 | imports: [ 18 | CommonModule, 19 | RatingRoutingModule, 20 | FormsModule, 21 | SharedModule, 22 | ReactiveFormsModule, 23 | MatTabsModule, 24 | MatCardModule, 25 | NgRatingModule, 26 | HttpClientModule, 27 | MarkdownModule, 28 | ], 29 | }) 30 | export class RatingModule {} 31 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(waitForAsync(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'angular-rating'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('angular-rating'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('angular-rating app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/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-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/angular-rating'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | customLaunchers: { 25 | ChromeHeadlessNoSandbox: { 26 | base: 'ChromeHeadless', 27 | flags: ['--no-sandbox'] 28 | } 29 | }, 30 | port: 9876, 31 | colors: true, 32 | logLevel: config.LOG_INFO, 33 | autoWatch: true, 34 | browsers: ['Chrome'], 35 | singleRun: false, 36 | restartOnFileChange: true 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /src/app/rating/rating.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; 2 | import { Router, NavigationEnd, Event } from '@angular/router'; 3 | import { filter, map, takeUntil } from 'rxjs/operators'; 4 | import { Subject } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-rating', 8 | templateUrl: './rating.component.html', 9 | styleUrls: ['./rating.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class RatingComponent implements OnDestroy { 13 | private destroyed$ = new Subject(); 14 | public url: string; 15 | public links = [ 16 | { 17 | name: 'Example', 18 | link: 'examples', 19 | }, 20 | { 21 | name: 'API', 22 | link: 'api', 23 | }, 24 | ]; 25 | 26 | constructor(private route: Router) { 27 | this.route.events 28 | .pipe( 29 | filter((event: Event) => event instanceof NavigationEnd), 30 | map((event: NavigationEnd) => 31 | event.url !== '/' ? event.url.split('/')[2] : event.urlAfterRedirects.split('/')[2] 32 | ), 33 | takeUntil(this.destroyed$) 34 | ) 35 | .subscribe((url: string) => (this.url = url)); 36 | } 37 | 38 | ngOnDestroy(): void { 39 | this.destroyed$.next(); 40 | this.destroyed$.complete(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/darkoandreev/d-ng-rating.svg?token=dyC7xCjKChVxFuxWSFtn&branch=master)](https://travis-ci.com/darkoandreev/d-ng-rating) 2 | 3 | # Rating - Angular powered rating library 4 | 5 | ## Demo 6 | 7 | Please check rating component in action at http://darkoandreev.github.io/d-ng-rating 8 | 9 | ## Installation 10 | 11 | You need to have an Angular project with the supported Angular version. We strongly recommend using Angular CLI for this. 12 | 13 | 14 | After installing the above dependencies, install **d-ng-rating** via: 15 | 16 | ```html 17 | npm install d-ng-rating --save 18 | ``` 19 | 20 | Once installed you need to import our main module: 21 | 22 | ```javascript 23 | import { NgRatingModule } from 'd-ng-rating'; 24 | 25 | @NgModule({ 26 | ... 27 | imports: [NgRatingModule, ...], 28 | ... 29 | }) 30 | export class YourAppModule { 31 | } 32 | ``` 33 | **NOTE** There are few more packages that are dependencies of the library. 34 | 35 | ### Dependecies 36 | - @fortawesome/angular-fontawesome 37 | - @fortawesome/fontawesome-svg-core 38 | - @fortawesome/free-solid-svg-icons 39 | - @angular/cdk 40 | 41 | See more details in the [official documentation](https://github.com/darkoandreev/d-ng-rating/blob/master/projects/d-ng-rating/README.md) 42 | 43 | ## Supported browsers 44 | 45 | We support the same browsers and versions supported by Angular, whichever is more restrictive. See Angular browser support for more details, but on the high-level it should be something like: 46 | 47 | Chrome (45+) 48 | Firefox (40+) 49 | IE (10+) 50 | Edge (20+) 51 | Safari (7+) 52 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [true, "attribute", "app", "camelCase"], 13 | "component-selector": [true, "element", "app", "kebab-case"], 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "interface-name": false, 16 | "max-classes-per-file": false, 17 | "max-line-length": [true, 140], 18 | "member-access": false, 19 | "member-ordering": [ 20 | true, 21 | { 22 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 23 | } 24 | ], 25 | "no-consecutive-blank-lines": false, 26 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 27 | "no-empty": false, 28 | "no-inferrable-types": [true, "ignore-params"], 29 | "no-non-null-assertion": true, 30 | "no-redundant-jsdoc": true, 31 | "no-switch-case-fall-through": true, 32 | "no-var-requires": false, 33 | "object-literal-key-quotes": [true, "as-needed"], 34 | "object-literal-sort-keys": false, 35 | "ordered-imports": false, 36 | "quotemark": [true, "single"], 37 | "trailing-comma": false, 38 | "no-conflicting-lifecycle": true, 39 | "no-host-metadata-property": true, 40 | "no-input-rename": true, 41 | "no-inputs-metadata-property": true, 42 | "no-output-native": true, 43 | "no-output-on-prefix": true, 44 | "no-output-rename": true, 45 | "no-outputs-metadata-property": true, 46 | "template-banana-in-box": true, 47 | "template-no-negated-async": true, 48 | "use-lifecycle-interface": true, 49 | "use-pipe-transform-interface": true 50 | }, 51 | "rulesDirectory": ["codelyzer"] 52 | } 53 | -------------------------------------------------------------------------------- /src/app/rating/rating-demo/rating-demo.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 16 | 22 | {{ this.ratingLabel }} 23 | 24 | 25 | 26 | 32 |
33 | 34 |
35 |
36 | 37 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d-ng-rating", 3 | "version": "4.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "test-headless": "ng test d-ng-rating --watch=false --browsers=ChromeHeadless" 12 | }, 13 | "private": true, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "pretty-quick --staged", 17 | "pre-push": "ng build d-ng-rating --prod" 18 | } 19 | }, 20 | "dependencies": { 21 | "@angular-devkit/core": "^13.3.11", 22 | "@angular/animations": "~13.4.0", 23 | "@angular/cdk": "^13.3.9", 24 | "@angular/common": "^13.4.0", 25 | "@angular/compiler": "~13.4.0", 26 | "@angular/core": "~13.4.0", 27 | "@angular/forms": "~13.4.0", 28 | "@angular/material": "^13.3.9", 29 | "@angular/platform-browser": "~13.4.0", 30 | "@angular/platform-browser-dynamic": "~13.4.0", 31 | "@angular/router": "~13.4.0", 32 | "@fortawesome/angular-fontawesome": "^0.10.2", 33 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 34 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 35 | "ngx-clipboard": "^14.0.1", 36 | "ngx-markdown": "^12.0.1", 37 | "rxjs": "~6.6.3", 38 | "tslib": "^2.6.2", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~13.3.11", 43 | "@angular-eslint/builder": "13.5.0", 44 | "@angular-eslint/eslint-plugin": "13.5.0", 45 | "@angular-eslint/eslint-plugin-template": "13.5.0", 46 | "@angular-eslint/schematics": "13.5.0", 47 | "@angular-eslint/template-parser": "13.5.0", 48 | "@angular/cli": "~13.3.11", 49 | "@angular/compiler-cli": "~13.4.0", 50 | "@angular/flex-layout": "^13.0.0-beta.38", 51 | "@angular/language-service": "~13.4.0", 52 | "@types/jasmine": "~3.6.0", 53 | "@types/jasminewd2": "~2.0.3", 54 | "@types/node": "^12.11.1", 55 | "@typescript-eslint/eslint-plugin": "5.27.1", 56 | "@typescript-eslint/parser": "5.27.1", 57 | "codelyzer": "^6.0.0", 58 | "eslint": "^8.17.0", 59 | "husky": "^4.2.3", 60 | "jasmine-core": "~3.6.0", 61 | "jasmine-spec-reporter": "~5.0.0", 62 | "karma": "~6.3.2", 63 | "karma-chrome-launcher": "~3.1.0", 64 | "karma-coverage-istanbul-reporter": "~3.0.2", 65 | "karma-jasmine": "~4.0.0", 66 | "karma-jasmine-html-reporter": "^1.5.0", 67 | "ng-packagr": "^13.3.1", 68 | "prettier": "^2.0.1", 69 | "pretty-quick": "^2.0.1", 70 | "protractor": "~7.0.0", 71 | "ts-node": "~7.0.0", 72 | "tslint": "~6.1.0", 73 | "tslint-config-prettier": "^1.18.0", 74 | "typescript": "~4.6.4" 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/rating/rating-demo/rating-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-rating-demo', 6 | templateUrl: './rating-demo.component.html', 7 | styleUrls: ['./rating-demo.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | }) 10 | export class RatingDemoComponent implements OnInit { 11 | form: FormGroup; 12 | rating = 5; 13 | ratingLabel: string; 14 | 15 | firstSnippet = ` 16 | 17 | `; 18 | 19 | secondSnippet = ` 20 |
21 | 22 |
23 | `; 24 | thirdSnippet = ` 25 | 26 | `; 27 | 28 | fourthSnippet = ` 29 | 35 | {{ this.ratingLabel }} 36 | 37 | `; 38 | fifthSnippet = ` 39 | 40 | `; 41 | 42 | sixthSnippet = ` 43 | 44 | 45 | 46 | 47 | 48 | `; 49 | 50 | customTeplateCssSnippet = ` 51 | .star { 52 | font-size: 1.5rem; 53 | color: #b0c4de; 54 | } 55 | 56 | .filled { 57 | cursor: pointer; 58 | color: #1e90ff; 59 | } 60 | `; 61 | 62 | basicRatingTypescript = ` 63 | import {Component} from '@angular/core'; 64 | 65 | /** 66 | * @title Basic rating overview 67 | */ 68 | @Component({ 69 | selector: 'rating-overview-example', 70 | templateUrl: 'rating-overview-example.html', 71 | styleUrls: ['rating-overview-example.css'], 72 | }) 73 | export class NgRatingOverviewExample {} 74 | `; 75 | 76 | basicRatingLabelTypescript = ` 77 | import {Component} from '@angular/core'; 78 | 79 | /** 80 | * @title Basic rating with label 81 | */ 82 | @Component({ 83 | selector: 'rating-label-overview-example', 84 | templateUrl: 'rating-label-overview-example.html', 85 | styleUrls: ['rating-label-overview-example.css'], 86 | }) 87 | export class NgRatingLabelOverviewExample { 88 | public ratingLabel: string; 89 | } 90 | `; 91 | ratingFormTypescript = ` 92 | import {Component} from '@angular/core'; 93 | 94 | /** 95 | * @title Basic rating within form 96 | */ 97 | @Component({ 98 | selector: 'rating-form-overview-example', 99 | templateUrl: 'rating-form-overview-example.html', 100 | styleUrls: ['rating-form-overview-example.css'], 101 | }) 102 | export class NgRatingFormOverviewExample { 103 | form: FormGroup; 104 | constructor(private formBuilder: FormBuilder) {} 105 | 106 | ngOnInit(): void { 107 | this.form = this.formBuilder.group({ 108 | ratingControl: [3, Validators.required], 109 | }); 110 | } 111 | }`; 112 | 113 | disabledRatingTypescript = ` 114 | import {Component} from '@angular/core'; 115 | 116 | /** 117 | * @title Disabled rating 118 | */ 119 | @Component({ 120 | selector: 'rating-disabled-overview-example', 121 | templateUrl: 'rating-disabled-overview-example.html', 122 | styleUrls: ['rating-disabled-overview-example.css'], 123 | }) 124 | export class NgRatingDisabledOverviewExample { 125 | } 126 | `; 127 | readonlyRatingTypescript = ` 128 | import {Component} from '@angular/core'; 129 | 130 | /** 131 | * @title Readonly rating 132 | */ 133 | @Component({ 134 | selector: 'rating-readonly-overview-example', 135 | templateUrl: 'rating-readonly-overview-example.html', 136 | styleUrls: ['rating-readonly-overview-example.css'], 137 | }) 138 | export class NgRatingReadonlyOverviewExample { 139 | public rating: number = 5; 140 | }`; 141 | 142 | customTemplateRatingTypescript = ` 143 | import {Component} from '@angular/core'; 144 | 145 | /** 146 | * @title Custom template rating 147 | */ 148 | @Component({ 149 | selector: 'rating-template-overview-example', 150 | templateUrl: 'rating-template-overview-example.html', 151 | styleUrls: ['rating-template-overview-example.css'], 152 | }) 153 | export class NgRatingTemplateOverviewExample { 154 | public ratingLabel: string; 155 | }`; 156 | 157 | cancelableRatingHtml = ` 158 | 159 | `; 160 | cancelableRatingTypescript = ` 161 | import {Component} from '@angular/core'; 162 | 163 | /** 164 | * @title Cancelable rating 165 | */ 166 | @Component({ 167 | selector: 'rating-cancel-overview-example', 168 | templateUrl: 'rating-cancel-overview-example.html', 169 | styleUrls: ['rating-cancel-overview-example.css'], 170 | }) 171 | export class NgRatingCancelOverviewExample { 172 | } 173 | `; 174 | constructor(private formBuilder: FormBuilder) {} 175 | 176 | ngOnInit(): void { 177 | this.form = this.formBuilder.group({ 178 | ratingControl: [3, Validators.required], 179 | }); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-rating": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/angular-rating", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets", 28 | { 29 | "glob": "**/*.md", 30 | "input": "projects/d-ng-rating/src/lib/", 31 | "output": "/docs/" 32 | } 33 | ], 34 | "styles": [ 35 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 36 | "src/styles.scss", 37 | "./node_modules/prismjs/themes/prism-okaidia.css" 38 | ], 39 | "scripts": [ 40 | "./node_modules/prismjs/prism.js", 41 | "./node_modules/prismjs/components/prism-csharp.min.js", 42 | "./node_modules/prismjs/components/prism-css.min.js" 43 | ], 44 | "vendorChunk": true, 45 | "extractLicenses": false, 46 | "buildOptimizer": false, 47 | "sourceMap": true, 48 | "optimization": false, 49 | "namedChunks": true 50 | }, 51 | "configurations": { 52 | "production": { 53 | "fileReplacements": [ 54 | { 55 | "replace": "src/environments/environment.ts", 56 | "with": "src/environments/environment.prod.ts" 57 | } 58 | ], 59 | "optimization": true, 60 | "outputHashing": "all", 61 | "sourceMap": false, 62 | "namedChunks": false, 63 | "extractLicenses": true, 64 | "vendorChunk": false, 65 | "buildOptimizer": true, 66 | "budgets": [ 67 | { 68 | "type": "initial", 69 | "maximumWarning": "2mb", 70 | "maximumError": "5mb" 71 | }, 72 | { 73 | "type": "anyComponentStyle", 74 | "maximumWarning": "6kb", 75 | "maximumError": "10kb" 76 | } 77 | ] 78 | } 79 | } 80 | }, 81 | "serve": { 82 | "builder": "@angular-devkit/build-angular:dev-server", 83 | "options": { 84 | "browserTarget": "angular-rating:build" 85 | }, 86 | "configurations": { 87 | "production": { 88 | "browserTarget": "angular-rating:build:production" 89 | } 90 | } 91 | }, 92 | "extract-i18n": { 93 | "builder": "@angular-devkit/build-angular:extract-i18n", 94 | "options": { 95 | "browserTarget": "angular-rating:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular-devkit/build-angular:karma", 100 | "options": { 101 | "main": "src/test.ts", 102 | "polyfills": "src/polyfills.ts", 103 | "tsConfig": "tsconfig.spec.json", 104 | "karmaConfig": "karma.conf.js", 105 | "assets": ["src/favicon.ico", "src/assets"], 106 | "styles": ["./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", "src/styles.scss"], 107 | "scripts": [] 108 | } 109 | }, 110 | "e2e": { 111 | "builder": "@angular-devkit/build-angular:protractor", 112 | "options": { 113 | "protractorConfig": "e2e/protractor.conf.js", 114 | "devServerTarget": "angular-rating:serve" 115 | }, 116 | "configurations": { 117 | "production": { 118 | "devServerTarget": "angular-rating:serve:production" 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "d-ng-rating": { 125 | "projectType": "library", 126 | "root": "projects/d-ng-rating", 127 | "sourceRoot": "projects/d-ng-rating/src", 128 | "prefix": "lib", 129 | "architect": { 130 | "build": { 131 | "builder": "@angular-devkit/build-angular:ng-packagr", 132 | "options": { 133 | "tsConfig": "projects/d-ng-rating/tsconfig.lib.json", 134 | "project": "projects/d-ng-rating/ng-package.json" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "tsConfig": "projects/d-ng-rating/tsconfig.lib.prod.json" 139 | } 140 | } 141 | }, 142 | "test": { 143 | "builder": "@angular-devkit/build-angular:karma", 144 | "options": { 145 | "main": "projects/d-ng-rating/src/test.ts", 146 | "tsConfig": "projects/d-ng-rating/tsconfig.spec.json", 147 | "karmaConfig": "projects/d-ng-rating/karma.conf.js", 148 | "codeCoverage": true 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "defaultProject": "angular-rating" 155 | } 156 | -------------------------------------------------------------------------------- /projects/d-ng-rating/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Rating is Angular component that helps visualising and interacting with a star rating bar. 4 | 5 | --- 6 | 7 | ## Usage 8 | 9 | ### Basic rating 10 | 11 | In order to create a rating component, we simply have to define it in our component's template: 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | We need to define size of our rating items and we can do it by defining **size** mandatory property. 18 | 19 | ### Result label 20 | 21 | We can define rating result label that shows clicked rate item. 22 | 23 | ```html 24 | 25 | {{ this.ratingLabel }} 26 | 27 | ``` 28 | 29 | ### Using form 30 | 31 | We can use single form for the rating component. Rating items are set to **type='button'** in order to prevent 32 | sumibssion of the form before rating is completed. 33 | 34 | ```html 35 |
36 | 37 |
38 | ``` 39 | 40 | ### Cancelable rating 41 | 42 | By setting `showCancelIcon` property to **true**, we are activating new functionality for canceling current rating. 43 | For example, we can cancel our rating if we rate wrong. 44 | We can change rating cancel icon by using [FontAwesome icons](https://www.npmjs.com/package/@fortawesome/angular-fontawesome) and input property **cancelIcon**. 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | OR 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | ### Custom template 57 | 58 | In order to use your own rating template, you can define it by using **ng-template** and **hovered** template variable. 59 | Inside the template virtual element you need to define your rating item element and styling (item color, hovered item color). 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | ### Keyboard interaction 70 | 71 | - ArrowUp, ArrowRight: Moves to next rating item 72 | - ArrowDown, ArrowLeft: Moves to previous rating item 73 | - Home: Moves to the first step header 74 | - End: Moves to the last step header 75 | 76 | --- 77 | 78 | ## API 79 | 80 | Please find the related API below. 81 | 82 | ### Reference 83 | 84 | ```javascript 85 | import { NgRatingModule } from 'd-ng-rating'; 86 | ``` 87 | 88 | ### NgRatingComponent 89 | 90 | ```html 91 | Selector: d-ng-rating 92 | ``` 93 | 94 | | Property | Type | Description | Default value | 95 | | ------------------------- | --------------------------- | ---------------------------------------------------------------- | ----------------- | 96 | | @Input() id | string | Unique id of the element. | Auto-generated | 97 | | @Input('aria-label') | string | Used to set the aria-label attribute on the rating. | star | 98 | | @Input('aria-labelledby') | string | Used to set the aria-labelledby attribute on the rating. | Star rating | 99 | | @Input() rating | number | Used to set a number of initially selected items. | 0 | 100 | | @Input() size | number | Used to set maximal rating that can be given. | 5 | 101 | | @Input() readonly | boolean | If true, the rating can't be changed. | false | 102 | | @Input() disabled | boolean | Used to set disabled property for rating. | false | 103 | | @Input() showCancelIcon | boolean | Whether a cancel icon will be shown for canceling rating. | false | 104 | | @Input() icon | IconDefinition | Used to set icon for rating item. FontAwesome used for icons. | faStar | 105 | | @Input() cancelIcon | IconDefinition | Used to set icon for cancel button. FontAwesome used for icons. | faBan | 106 | | @Output() rateChange | EventEmitter | Event emitted when the rate is changed (rating item is clicked). | No default value. | 107 | | @Output() rateCancel | EventEmitter | Event emitted when rating is canceled. | No default value | 108 | | @Input() ratingTemplate | TemplateRef | Define custom template and pass as reference. | No default value | 109 | 110 | ### Dependency 111 | 112 | In order to use our rating star and cancel icons you will need to install **@fortawesome/angular-fontawesome** package as 113 | peer dependency. You can find this npm package [HERE](https://www.npmjs.com/package/@fortawesome/angular-fontawesome). 114 | If you want to use your own custom rating template, then you don't need to install the peer dependency. 115 | 116 | #### List of dependencies: 117 | - @fortawesome/angular-fontawesome 118 | - @fortawesome/fontawesome-svg-core 119 | - @fortawesome/free-solid-svg-icons 120 | - @angular/cdk 121 | 122 | ### Interfaces 123 | 124 | #### IRatingContext 125 | 126 | ```javascript 127 | export interface IRatingContext { 128 | hovered: boolean; 129 | } 130 | ``` 131 | 132 | --- 133 | 134 | ## Accessibility 135 | 136 | The rating component has role attribute set to **slider**. 137 | The component exposes aria inputs in order to provide accessible experience. The components should be given a meaningful label via aria-label or aria-labelledby. By default aria-label is set to **star** and aria-labelledby to **Star rating**. 138 | -------------------------------------------------------------------------------- /src/assets/docs/ng-rating.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Rating is Angular component that helps visualising and interacting with a star rating bar. 4 | 5 | --- 6 | 7 | ## Usage 8 | 9 | ### Basic rating 10 | 11 | In order to create a rating component, we simply have to define it in our component's template: 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | We need to define size of our rating items and we can do it by defining **size** mandatory property. 18 | 19 | ### Result label 20 | 21 | We can define rating result label that shows clicked rate item. 22 | 23 | ```html 24 | 25 | {{ this.ratingLabel }} 26 | 27 | ``` 28 | 29 | ### Using form 30 | 31 | We can use single form for the rating component. Rating items are set to **type='button'** in order to prevent 32 | sumibssion of the form before rating is completed. 33 | 34 | ```html 35 |
36 | 37 |
38 | ``` 39 | 40 | ### Cancelable rating 41 | 42 | By setting `showCancelIcon` property to **true**, we are activating new functionality for canceling current rating. 43 | For example, we can cancel our rating if we rate wrong. 44 | We can change rating cancel icon by using [FontAwesome icons](https://www.npmjs.com/package/@fortawesome/angular-fontawesome) and input property **cancelIcon**. 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | OR 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | ### Custom template 57 | 58 | In order to use your own rating template, you can define it by using **ng-template** and **hovered** template variable. 59 | Inside the template virtual element you need to define your rating item element and styling (item color, hovered item color). 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | ### Keyboard interaction 70 | 71 | - ArrowUp, ArrowRight: Moves to next rating item 72 | - ArrowDown, ArrowLeft: Moves to previous rating item 73 | - Home: Moves to the first step header 74 | - End: Moves to the last step header 75 | 76 | --- 77 | 78 | ## API 79 | 80 | Please find the related API below. 81 | 82 | ### Reference 83 | 84 | ```javascript 85 | import { NgRatingModule } from 'd-ng-rating'; 86 | ``` 87 | 88 | ### NgRatingComponent 89 | 90 | ```html 91 | Selector: d-ng-rating 92 | ``` 93 | 94 | | Property | Type | Description | Default value | 95 | | ------------------------- | --------------------------- | ---------------------------------------------------------------- | ----------------- | 96 | | @Input() id | string | Unique id of the element. | Auto-generated | 97 | | @Input('aria-label') | string | Used to set the aria-label attribute on the rating. | star | 98 | | @Input('aria-labelledby') | string | Used to set the aria-labelledby attribute on the rating. | Star rating | 99 | | @Input() rating | number | Used to set a number of initially selected items. | 0 | 100 | | @Input() size | number | Used to set maximal rating that can be given. | 5 | 101 | | @Input() readonly | boolean | If true, the rating can't be changed. | false | 102 | | @Input() disabled | boolean | Used to set disabled property for rating. | false | 103 | | @Input() showCancelIcon | boolean | Whether a cancel icon will be shown for canceling rating. | false | 104 | | @Input() icon | IconDefinition | Used to set icon for rating item. FontAwesome used for icons. | faStar | 105 | | @Input() cancelIcon | IconDefinition | Used to set icon for cancel button. FontAwesome used for icons. | faBan | 106 | | @Output() rateChange | EventEmitter | Event emitted when the rate is changed (rating item is clicked). | No default value. | 107 | | @Output() rateCancel | EventEmitter | Event emitted when rating is canceled. | No default value | 108 | | @Input() ratingTemplate | TemplateRef | Define custom template and pass as reference. | No default value | 109 | 110 | ### Dependency 111 | 112 | In order to use our rating star and cancel icons you will need to install **@fortawesome/angular-fontawesome** package as 113 | peer dependency. You can find this npm package [HERE](https://www.npmjs.com/package/@fortawesome/angular-fontawesome). 114 | If you want to use your own custom rating template, then you don't need to install the peer dependency. 115 | 116 | #### List of dependencies: 117 | - @fortawesome/angular-fontawesome 118 | - @fortawesome/fontawesome-svg-core 119 | - @fortawesome/free-solid-svg-icons 120 | - @angular/cdk 121 | 122 | ### Interfaces 123 | 124 | #### IRatingContext 125 | 126 | ```javascript 127 | export interface IRatingContext { 128 | hovered: boolean; 129 | } 130 | ``` 131 | 132 | --- 133 | 134 | ## Accessibility 135 | 136 | The rating component has role attribute set to **slider**. 137 | The component exposes aria inputs in order to provide accessible experience. The components should be given a meaningful label via aria-label or aria-labelledby. By default aria-label is set to **star** and aria-labelledby to **Star rating**. 138 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Rating is Angular component that helps visualising and interacting with a star rating bar. 4 | 5 | --- 6 | 7 | ## Usage 8 | 9 | ### Basic rating 10 | 11 | In order to create a rating component, we simply have to define it in our component's template: 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | We need to define size of our rating items and we can do it by defining **size** mandatory property. 18 | 19 | ### Result label 20 | 21 | We can define rating result label that shows clicked rate item. 22 | 23 | ```html 24 | 25 | {{ this.ratingLabel }} 26 | 27 | ``` 28 | 29 | ### Using form 30 | 31 | We can use single form for the rating component. Rating items are set to **type='button'** in order to prevent 32 | sumibssion of the form before rating is completed. 33 | 34 | ```html 35 |
36 | 37 |
38 | ``` 39 | 40 | ### Cancelable rating 41 | 42 | By setting `showCancelIcon` property to **true**, we are activating new functionality for canceling current rating. 43 | For example, we can cancel our rating if we rate wrong. 44 | We can change rating cancel icon by using [FontAwesome icons](https://www.npmjs.com/package/@fortawesome/angular-fontawesome) and input property **cancelIcon**. 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | OR 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | ### Custom template 57 | 58 | In order to use your own rating template, you can define it by using **ng-template** and **hovered** template variable. 59 | Inside the template virtual element you need to define your rating item element and styling (item color, hovered item color). 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | ### Keyboard interaction 70 | 71 | - ArrowUp, ArrowRight: Moves to next rating item 72 | - ArrowDown, ArrowLeft: Moves to previous rating item 73 | - Home: Moves to the first step header 74 | - End: Moves to the last step header 75 | 76 | --- 77 | 78 | ## API 79 | 80 | Please find the related API below. 81 | 82 | ### Reference 83 | 84 | ```javascript 85 | import { NgRatingModule } from 'd-ng-rating'; 86 | ``` 87 | 88 | ### NgRatingComponent 89 | 90 | ```html 91 | Selector: d-ng-rating 92 | ``` 93 | 94 | | Property | Type | Description | Default value | 95 | | ------------------------- | --------------------------- | ---------------------------------------------------------------- | ----------------- | 96 | | @Input() id | string | Unique id of the element. | Auto-generated | 97 | | @Input('aria-label') | string | Used to set the aria-label attribute on the rating. | star | 98 | | @Input('aria-labelledby') | string | Used to set the aria-labelledby attribute on the rating. | Star rating | 99 | | @Input() rating | number | Used to set a number of initially selected items. | 0 | 100 | | @Input() size | number | Used to set maximal rating that can be given. | 5 | 101 | | @Input() readonly | boolean | If true, the rating can't be changed. | false | 102 | | @Input() disabled | boolean | Used to set disabled property for rating. | false | 103 | | @Input() showCancelIcon | boolean | Whether a cancel icon will be shown for canceling rating. | false | 104 | | @Input() icon | IconDefinition | Used to set icon for rating item. FontAwesome used for icons. | faStar | 105 | | @Input() cancelIcon | IconDefinition | Used to set icon for cancel button. FontAwesome used for icons. | faBan | 106 | | @Output() rateChange | EventEmitter | Event emitted when the rate is changed (rating item is clicked). | No default value. | 107 | | @Output() rateCancel | EventEmitter | Event emitted when rating is canceled. | No default value | 108 | | @Input() ratingTemplate | TemplateRef | Define custom template and pass as reference. | No default value | 109 | 110 | ### Dependency 111 | 112 | In order to use our rating star and cancel icons you will need to install **@fortawesome/angular-fontawesome** package as 113 | peer dependency. You can find this npm package [HERE](https://www.npmjs.com/package/@fortawesome/angular-fontawesome). 114 | If you want to use your own custom rating template, then you don't need to install the peer dependency. 115 | 116 | #### List of dependencies: 117 | - @fortawesome/angular-fontawesome 118 | - @fortawesome/fontawesome-svg-core 119 | - @fortawesome/free-solid-svg-icons 120 | - @angular/cdk 121 | 122 | ### Interfaces 123 | 124 | #### IRatingContext 125 | 126 | ```javascript 127 | export interface IRatingContext { 128 | hovered: boolean; 129 | } 130 | ``` 131 | 132 | --- 133 | 134 | ## Accessibility 135 | 136 | The rating component has role attribute set to **slider**. 137 | The component exposes aria inputs in order to provide accessible experience. The components should be given a meaningful label via aria-label or aria-labelledby. By default aria-label is set to **star** and aria-labelledby to **Star rating**. 138 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | Output, 5 | EventEmitter, 6 | forwardRef, 7 | Input, 8 | HostBinding, 9 | ContentChild, 10 | OnChanges, 11 | SimpleChanges, 12 | HostListener, 13 | ViewEncapsulation, 14 | TemplateRef, 15 | } from '@angular/core'; 16 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 17 | import { faStar, faBan, IconDefinition } from '@fortawesome/free-solid-svg-icons'; 18 | import { RATING_SIZE_ERROR, RATE_SET_ERROR } from './ng-rating.error'; 19 | import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion'; 20 | import { NgRatingLabelDirective } from './ng-rating-label.directive'; 21 | import { Key } from './util/key'; 22 | 23 | /** 24 | * Provider that allows the rating component to register as a ControlValueAccessor. 25 | * @docs-private @internal 26 | */ 27 | export const NG_RATING_VALUE_ACCESSOR: any = { 28 | provide: NG_VALUE_ACCESSOR, 29 | useExisting: forwardRef(() => NgRatingComponent), 30 | multi: true, 31 | }; 32 | 33 | /** 34 | * Rating item model. 35 | */ 36 | export interface IRatingContext { 37 | hovered: boolean; 38 | } 39 | 40 | let UNIQUE_ID = 0; 41 | 42 | /** 43 | * Rating components is a star based selection input. 44 | * A star rating usually consists of images of stars that can be used to rate a particular item. 45 | * A mouse user hovers over the stars and clicks one to select it. 46 | * For example, if the user clicks on the third star from the left, the rating of the item is 3 of 5 stars. 47 | * ``` 48 | * @example 49 | * 50 | * {{ this.ratingLabel }} 51 | * 52 | * ``` 53 | * 54 | * @export 55 | */ 56 | @Component({ 57 | selector: 'd-ng-rating', 58 | templateUrl: './ng-rating.component.html', 59 | styleUrls: ['./ng-rating.component.scss'], 60 | changeDetection: ChangeDetectionStrategy.OnPush, 61 | providers: [NG_RATING_VALUE_ACCESSOR], 62 | encapsulation: ViewEncapsulation.None, 63 | }) 64 | export class NgRatingComponent implements OnChanges, ControlValueAccessor { 65 | /** @hidden @internal */ 66 | public ratings: IRatingContext[]; 67 | 68 | /** Currently selected rating item index 69 | * @hidden @internal 70 | */ 71 | public _selectedIndex = -1; 72 | 73 | /** @hidden @internal */ 74 | @ContentChild(NgRatingLabelDirective) public ratingLabelTemplate: NgRatingLabelDirective; 75 | 76 | @HostBinding('class.ng-star-rating') get starRatingClass(): boolean { 77 | return true; 78 | } 79 | 80 | /** A unique id for the rating input. If none is supplied, it will be auto-generated. */ 81 | @HostBinding('attr.id') 82 | @Input() 83 | public id = `ng-star-rating-${UNIQUE_ID++}`; 84 | 85 | /** 86 | * Attached to the aria-label attribute of the host element. 87 | * In most cases, aria-labelledby will take precedence so this may be omitted. 88 | */ 89 | @HostBinding('attr.aria-label') 90 | @Input('aria-label') 91 | public ariaLabel = 'star'; 92 | 93 | /** 94 | * Users can specify the `aria-labelledby` attribute which will be forwarded to the input element. 95 | */ 96 | @HostBinding('attr.aria-labelledby') 97 | @Input('aria-labelledby') 98 | public ariaLabelledby: string | undefined = 'Star rating'; 99 | 100 | @HostBinding('attr.aria-valuemin') get ariaValueMin(): number { 101 | return 0; 102 | } 103 | 104 | @HostBinding('attr.aria-valuenow') get ariaValueNow(): number { 105 | return this._selectedIndex + 1; 106 | } 107 | 108 | @HostBinding('attr.aria-valuetext') get ariaValueTextAttr(): string { 109 | return this.ariaValueText; 110 | } 111 | 112 | @HostBinding('attr.role') get role(): string { 113 | return 'slider'; 114 | } 115 | 116 | @HostBinding('attr.tabindex') get tabindexAttr(): number { 117 | return this.disabled ? -1 : 0; 118 | } 119 | 120 | /** 121 | * Gets/sets the `rating` for the component. 122 | * Determines selected rate items. 123 | * @example 124 | * ```html 125 | * 126 | * ``` 127 | * @memberOf NgRatingComponent 128 | */ 129 | @Input() 130 | public get rating(): number { 131 | return this._rating; 132 | } 133 | public set rating(value: number) { 134 | if (value <= 0) { 135 | RATE_SET_ERROR(); 136 | } 137 | this._rating = coerceNumberProperty(value); 138 | } 139 | private _rating: number; 140 | 141 | /** 142 | * Gets/sets the `size` for the component. 143 | * Sets max number of rate items. 144 | * @example 145 | * ```html 146 | * 147 | * ``` 148 | * @memberOf NgRatingComponent 149 | */ 150 | @Input() 151 | @HostBinding('attr.aria-valuemax') 152 | @HostBinding('attr.aria-setsize') 153 | public get size(): number { 154 | return this._size; 155 | } 156 | public set size(value: number) { 157 | if (value <= 0) { 158 | RATING_SIZE_ERROR(); 159 | } 160 | 161 | this._size = coerceNumberProperty(value); 162 | this.ratings = Array.from(new Array(value)).map(() => { 163 | const rating: IRatingContext = { 164 | hovered: false, 165 | }; 166 | return rating; 167 | }); 168 | } 169 | private _size = 5; 170 | 171 | /** 172 | * Gets/sets the `readonly` property. 173 | * Determines if the rate component is readonly. 174 | * By default it's **false**. 175 | * @example 176 | * ```html 177 | * 178 | * ``` 179 | * @memberOf NgRatingComponent 180 | */ 181 | @Input() 182 | @HostBinding('attr.aria-readonly') 183 | public get readonly(): boolean { 184 | return this._readonly; 185 | } 186 | public set readonly(value: boolean) { 187 | this._readonly = coerceBooleanProperty(value); 188 | } 189 | private _readonly = false; 190 | 191 | /** 192 | * Gets/sets the `disabled` property. 193 | * Whether the rate component is disabled. 194 | * By default rate items are clickable (disabled=false). 195 | * @example 196 | * ```html 197 | * 198 | * ``` 199 | * @memberOf NgRatingComponent 200 | */ 201 | @Input() 202 | @HostBinding('attr.aria-disabled') 203 | public get disabled(): boolean { 204 | return this._disabled; 205 | } 206 | public set disabled(value: boolean) { 207 | this._disabled = coerceBooleanProperty(value); 208 | } 209 | private _disabled = false; 210 | 211 | /** 212 | * Gets/sets the `showCancelIcon` property. 213 | * Whether the cancel (clear) icon is visible. 214 | * By default it's visible. 215 | * @example 216 | * ```html 217 | * 218 | * ``` 219 | * @memberOf NgRatingComponent 220 | */ 221 | @Input() 222 | public get showCancelIcon(): boolean { 223 | return this._showCancelIcon; 224 | } 225 | public set showCancelIcon(value: boolean) { 226 | this._showCancelIcon = coerceBooleanProperty(value); 227 | } 228 | private _showCancelIcon = false; 229 | 230 | /** 231 | * Gets/sets the `icon` for the rate item. 232 | * By default it's **faStar** FontAwesome icon. 233 | * @example 234 | * ```html 235 | * 236 | * ``` 237 | * @memberOf NgRatingComponent 238 | */ 239 | @Input() public icon: IconDefinition = faStar; 240 | 241 | /** 242 | * Gets/sets the `cancelIcon` for the component. 243 | * By default it uses **faBan** FontAwesome icon. 244 | * @example 245 | * ```html 246 | * 247 | * ``` 248 | * @memberOf NgRatingComponent 249 | */ 250 | @Input() public cancelIcon: IconDefinition = faBan; 251 | 252 | /** 253 | * An event that is emitted after the rate item is clicked and set. 254 | * Provides a number of clicked item - ex. 1,2,3, etc. 255 | * @example 256 | * ```html 257 | * 258 | * ``` 259 | */ 260 | @Output() public rateChange: EventEmitter = new EventEmitter(); 261 | 262 | /** 263 | * An event that is emitted after the rating is canceled (cleared). 264 | * @example 265 | * ```html 266 | * 267 | * ``` 268 | */ 269 | @Output() public rateCancel: EventEmitter = new EventEmitter(); 270 | 271 | /** 272 | * The template to override the way each star is displayed. 273 | * 274 | * Alternatively put an `` as the only child of your `` element 275 | * @example 276 | * ```html 277 | * 278 | * 279 | * 280 | * 281 | * 282 | * ``` 283 | */ 284 | 285 | @ContentChild(TemplateRef, { static: false }) ratingTemplateContent: TemplateRef; 286 | @Input() ratingTemplate: TemplateRef; 287 | 288 | ngOnChanges(changes: SimpleChanges): void { 289 | if ('rating' in changes && changes.rating.currentValue > 0) { 290 | this.ratingsHover(this.rating - 1); 291 | this._selectedIndex = this.rating - 1; 292 | } 293 | } 294 | 295 | /** @hidden @internal */ 296 | public hoveredItem(index: number): void { 297 | if (!this.readonly) { 298 | this.ratingsHover(index); 299 | } 300 | } 301 | 302 | /** @hidden @internal */ 303 | public handleClick(index: number): void { 304 | if (!this.readonly) { 305 | this.update(index); 306 | } 307 | } 308 | 309 | /** @hidden @internal */ 310 | public cancel(): void { 311 | this._selectedIndex = -1; 312 | this.ratings.forEach((item: IRatingContext) => (item.hovered = false)); 313 | this.rateCancel.emit(); 314 | } 315 | 316 | /** @hidden @internal */ 317 | @HostListener('mouseleave') 318 | public mouseLeave(): void { 319 | if (!this.readonly) { 320 | this.ratings.forEach((item: IRatingContext, index) => (item.hovered = !(index > this._selectedIndex))); 321 | } 322 | } 323 | 324 | @HostListener('blur') 325 | public blur(): void { 326 | this.onTouched(); 327 | } 328 | 329 | /** Handle rating using arrow keys and home/end keys */ 330 | @HostListener('keydown', ['$event']) 331 | public handleKeyDown(event: KeyboardEvent): void { 332 | switch (event.code) { 333 | case Key.ArrowDown: 334 | case Key.ArrowLeft: 335 | if (this._selectedIndex > -1) { 336 | this._selectedIndex--; 337 | this.update(this._selectedIndex); 338 | } 339 | break; 340 | case Key.ArrowUp: 341 | case Key.ArrowRight: 342 | if (this._selectedIndex < this.size - 1) { 343 | this._selectedIndex++; 344 | this.update(this._selectedIndex); 345 | } 346 | break; 347 | case Key.Home: 348 | this.update(0); 349 | break; 350 | case Key.End: 351 | this.update(this.size - 1); 352 | break; 353 | default: 354 | return; 355 | } 356 | 357 | event.preventDefault(); 358 | } 359 | 360 | /** @hidden @internal */ 361 | public writeValue(rating: number): void { 362 | if (rating) { 363 | this._controlValueAccessorChangeFn(rating); 364 | this.rating = rating; 365 | this._selectedIndex = rating - 1; 366 | this.ratingsHover(rating - 1); 367 | } 368 | } 369 | 370 | /** @hidden @internal */ 371 | public registerOnChange(fn: (value: any) => void): void { 372 | this._controlValueAccessorChangeFn = fn; 373 | } 374 | 375 | /** @hidden @internal */ 376 | public registerOnTouched(fn: any): void { 377 | this.onTouched = fn; 378 | } 379 | 380 | /** @hidden @internal */ 381 | public setDisabledState(value: boolean): void { 382 | this.disabled = value; 383 | } 384 | 385 | private update(index: number): void { 386 | this._selectedIndex = index; 387 | this.hoveredItem(this._selectedIndex); 388 | this.onTouched(); 389 | this.rateChange.emit(index + 1); 390 | } 391 | 392 | // Function to call when the rating changes. 393 | private _controlValueAccessorChangeFn: (value: any) => void = () => {}; 394 | 395 | // Function to call when the input is touched (when a star is clicked). 396 | private onTouched: () => any = () => {}; 397 | 398 | private ratingsHover(index: number): void { 399 | this.ratings.forEach((item: IRatingContext, i) => (item.hovered = index >= i)); 400 | } 401 | 402 | private get ariaValueText(): string { 403 | return `${this._selectedIndex + 1} out of ${this.size}`; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /projects/d-ng-rating/src/lib/ng-rating.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; 2 | import { NgRatingComponent } from './ng-rating.component'; 3 | import { Component, ViewChild, DebugElement, OnInit } from '@angular/core'; 4 | import { By } from '@angular/platform-browser'; 5 | import { NgRatingModule } from './ng-rating.module'; 6 | import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; 7 | import { Key } from './util/key'; 8 | 9 | const NG_STAR_RATING_CLASS = '.ng-star-rating'; 10 | const NG_STAR_RATING_ITEM_CLASS = '.ng-star-rating .d-ng-rating-item'; 11 | const NG_STAR_RATING_CANCEL_CLASS = '.ng-star-rating .d-ng-rating-cancel'; 12 | const NG_STAR_RATING_LABEL = '.ng-star-rating .d-ng-rating-label'; 13 | const NG_STAR_RATING_ITEM_ICON_CLASS = '.ng-star-rating .d-ng-rating-item .d-ng-rating-item-icon'; 14 | 15 | describe('NgRatingComponent', () => { 16 | let component: NgRatingTestComponent; 17 | let fixture: ComponentFixture; 18 | 19 | beforeEach(waitForAsync(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [NgRatingModule, FormsModule, ReactiveFormsModule], 22 | declarations: [ 23 | NgRatingTestComponent, 24 | NgRatingPreDefinedTestComponent, 25 | NgRatingReadonlyTestComponent, 26 | NgRatingControlValueAccessorTestComponent, 27 | NgRatingFormControlTestComponent, 28 | ], 29 | }).compileComponents(); 30 | })); 31 | 32 | describe('Basic ng rating', () => { 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(NgRatingTestComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | 43 | it('should set ratings size', () => { 44 | expect(component.size).toEqual(6); 45 | 46 | component.size = 4; 47 | fixture.detectChanges(); 48 | 49 | expect(component.size).toEqual(4); 50 | const ratingElement = fixture.debugElement.query(By.css(NG_STAR_RATING_CLASS)).nativeElement as Element; 51 | const ratingSizeAttr = ratingElement.getAttribute('ng-reflect-size'); 52 | expect(ratingSizeAttr).toEqual('4'); 53 | }); 54 | 55 | it('should set ratings array', () => { 56 | expect(component.size).toEqual(6); 57 | 58 | component.size = 4; 59 | fixture.detectChanges(); 60 | 61 | const ratings = component.ratingComponent.ratings; 62 | expect(ratings.length).toEqual(4); 63 | 64 | ratings.forEach((rating) => { 65 | expect(rating).toEqual({ hovered: false }); 66 | }); 67 | }); 68 | 69 | it('should set rating by clicking on item', () => { 70 | spyOn(component.ratingComponent, 'handleClick').and.callThrough(); 71 | 72 | let ratingItemElement = fixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[3]; 73 | ratingItemElement.triggerEventHandler('click', null); 74 | fixture.detectChanges(); 75 | const labelElement = fixture.debugElement.query(By.css(NG_STAR_RATING_LABEL)).nativeElement; 76 | 77 | expect(component.ratingLabel).toEqual(4); 78 | expect(component.ratingComponent.handleClick).toHaveBeenCalledWith(3); 79 | expect(labelElement.innerText).toEqual('4'); 80 | 81 | ratingItemElement = fixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[4]; 82 | ratingItemElement.triggerEventHandler('click', null); 83 | fixture.detectChanges(); 84 | 85 | expect(component.ratingLabel).toEqual(5); 86 | expect(labelElement.innerText).toEqual('5'); 87 | }); 88 | 89 | it('should clear rating by clicking on cancel (clear) button', () => { 90 | spyOn(component.ratingComponent.rateCancel, 'emit'); 91 | component.showCancelIcon = true; 92 | fixture.detectChanges(); 93 | 94 | const ratingItemElement = fixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[3]; 95 | ratingItemElement.triggerEventHandler('click', null); 96 | 97 | expect(component.ratingLabel).toEqual(4); 98 | 99 | const ratingCancelElement = fixture.debugElement.query(By.css(NG_STAR_RATING_CANCEL_CLASS)); 100 | ratingCancelElement.triggerEventHandler('click', null); 101 | 102 | expect(component.ratingComponent.rateCancel.emit).toHaveBeenCalledWith(); 103 | 104 | const ratings = component.ratingComponent.ratings; 105 | ratings.forEach((rating) => { 106 | expect(rating).toEqual({ hovered: false }); 107 | }); 108 | }); 109 | 110 | it('should show/hide cancel icon', () => { 111 | component.showCancelIcon = true; 112 | fixture.detectChanges(); 113 | let ratingCancelElement = fixture.debugElement.query(By.css(NG_STAR_RATING_CANCEL_CLASS)); 114 | 115 | expect(ratingCancelElement.nativeElement).toBeTruthy(); 116 | expect(component.ratingComponent.showCancelIcon).toBeTruthy(); 117 | 118 | component.showCancelIcon = false; 119 | fixture.detectChanges(); 120 | 121 | ratingCancelElement = fixture.debugElement.query(By.css(NG_STAR_RATING_CANCEL_CLASS)); 122 | 123 | expect(ratingCancelElement).toBeFalsy(); 124 | expect(component.ratingComponent.showCancelIcon).toBeFalsy(); 125 | }); 126 | 127 | it('should dispatch mouseleave event', () => { 128 | const ratingItemDebugElement = fixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[3]; 129 | const ratingComponentDebugElement = fixture.debugElement.query(By.directive(NgRatingComponent)); 130 | const ratingItemIconDebugElements = fixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_ICON_CLASS)); 131 | 132 | ratingItemDebugElement.triggerEventHandler('mouseenter', null); 133 | fixture.detectChanges(); 134 | 135 | expect(ratingComponentDebugElement.nativeElement).toBeTruthy(); 136 | 137 | ratingItemIconDebugElements.forEach((icon, index) => { 138 | const element: Element = icon.nativeElement; 139 | if (index <= 3) { 140 | expect(element.classList).toContain('d-ng-rating-item-icon-hover'); 141 | } 142 | }); 143 | 144 | ratingComponentDebugElement.triggerEventHandler('mouseleave', null); 145 | fixture.detectChanges(); 146 | 147 | ratingItemIconDebugElements.forEach((icon, index) => { 148 | const element: Element = icon.nativeElement; 149 | if (index <= 3) { 150 | expect(element.classList).not.toContain('d-ng-rating-item-icon-hover'); 151 | } 152 | }); 153 | }); 154 | 155 | it('should throw error if ng rating size is less than zero', () => { 156 | expect(() => { 157 | component.size = -1; 158 | fixture.detectChanges(); 159 | }).toThrowError(/Rating size must be greater than zero./); 160 | }); 161 | }); 162 | 163 | describe('Rating with pre-defined rate', () => { 164 | let preDefinedfixture: ComponentFixture; 165 | let preDefinedComponent: NgRatingPreDefinedTestComponent; 166 | 167 | beforeEach(() => { 168 | preDefinedfixture = TestBed.createComponent(NgRatingPreDefinedTestComponent); 169 | preDefinedComponent = preDefinedfixture.componentInstance; 170 | preDefinedfixture.detectChanges(); 171 | }); 172 | 173 | it('should create component', () => { 174 | expect(preDefinedComponent).toBeTruthy(); 175 | }); 176 | 177 | it('should pre-define rating for d-ng-rating component', () => { 178 | expect(preDefinedComponent.rating).toBe(3); 179 | const rating = spyOnProperty(preDefinedComponent.ratingComponent, 'rating', 'set').and.callThrough(); 180 | preDefinedComponent.rating = 5; 181 | 182 | preDefinedfixture.detectChanges(); 183 | 184 | expect(rating).toHaveBeenCalledWith(5); 185 | expect(preDefinedComponent.rating).toBe(5); 186 | expect(preDefinedComponent.ratingComponent.rating).toBe(5); 187 | }); 188 | 189 | it('should throw error if ng rating size is less than zero', () => { 190 | expect(() => { 191 | preDefinedComponent.rating = -1; 192 | preDefinedfixture.detectChanges(); 193 | }).toThrowError(/Rate definition must be greather than zero./); 194 | }); 195 | }); 196 | 197 | describe('Rating states', () => { 198 | let readonlyFixture: ComponentFixture; 199 | let readonlyComponent: NgRatingReadonlyTestComponent; 200 | 201 | beforeEach(() => { 202 | readonlyFixture = TestBed.createComponent(NgRatingReadonlyTestComponent); 203 | readonlyComponent = readonlyFixture.componentInstance; 204 | readonlyFixture.detectChanges(); 205 | }); 206 | 207 | it('should set readonly property', () => { 208 | let ratingItemElement = readonlyFixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[3]; 209 | ratingItemElement.triggerEventHandler('click', null); 210 | readonlyFixture.detectChanges(); 211 | 212 | expect(ratingItemElement.nativeElement.getAttribute('aria-selected')).toBeTruthy(); 213 | expect(ratingItemElement.nativeElement.getAttribute('aria-selected')).toEqual('true'); 214 | 215 | readonlyComponent.readonly = true; 216 | 217 | readonlyFixture.detectChanges(); 218 | ratingItemElement = readonlyFixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[4]; 219 | ratingItemElement.triggerEventHandler('click', null); 220 | 221 | expect(readonlyComponent.ratingComponent.readonly).toBeTruthy(); 222 | expect(ratingItemElement.nativeElement.getAttribute('aria-selected')).toEqual('false'); 223 | }); 224 | 225 | it('should set disabled property', () => { 226 | readonlyComponent.disabled = true; 227 | readonlyFixture.detectChanges(); 228 | 229 | expect(readonlyComponent.ratingComponent.disabled).toBeTruthy(); 230 | 231 | const ratingItemElements = readonlyFixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS)); 232 | const ratingElement = readonlyFixture.debugElement.query(By.directive(NgRatingComponent)); 233 | 234 | expect(ratingElement.nativeElement.getAttribute('aria-disabled')).toEqual('true'); 235 | ratingItemElements.forEach((debugElement) => { 236 | const element: Element = debugElement.nativeElement; 237 | expect(element.classList).toContain('d-ng-rating-item-disabled'); 238 | }); 239 | }); 240 | }); 241 | 242 | describe('Ng rating using ngModel', () => { 243 | let cvaFixture: ComponentFixture; 244 | let cvaComponent: NgRatingControlValueAccessorTestComponent; 245 | 246 | beforeEach(() => { 247 | cvaFixture = TestBed.createComponent(NgRatingControlValueAccessorTestComponent); 248 | cvaComponent = cvaFixture.componentInstance; 249 | cvaFixture.detectChanges(); 250 | }); 251 | 252 | it('should create component', () => { 253 | expect(cvaComponent).toBeTruthy(); 254 | }); 255 | 256 | it('should set rating with ngModel', fakeAsync(() => { 257 | const writeValueSpy = spyOn(cvaComponent.ratingComponent, 'writeValue').and.callThrough(); 258 | cvaComponent.rating = 4; 259 | cvaFixture.detectChanges(); 260 | tick(); 261 | 262 | expect(cvaComponent.ratingComponent.rating).toEqual(4); 263 | expect(writeValueSpy).toHaveBeenCalledWith(4); 264 | })); 265 | }); 266 | 267 | describe('Ng rating using form group', () => { 268 | let formFixture: ComponentFixture; 269 | let formComponent: NgRatingFormControlTestComponent; 270 | 271 | beforeEach(() => { 272 | formFixture = TestBed.createComponent(NgRatingFormControlTestComponent); 273 | formComponent = formFixture.componentInstance; 274 | formFixture.detectChanges(); 275 | }); 276 | 277 | it('should mark control as touched on blur', () => { 278 | const element: DebugElement = formFixture.debugElement.query(By.directive(NgRatingComponent)); 279 | expect(element.nativeElement.classList).toContain('ng-untouched'); 280 | 281 | element.triggerEventHandler('blur', null); 282 | formFixture.detectChanges(); 283 | expect(element.nativeElement.classList).toContain('ng-touched'); 284 | }); 285 | 286 | it('should disabled rating when a control is disabled', () => { 287 | const ratingItemDebugElement = formFixture.debugElement.queryAll(By.css(NG_STAR_RATING_ITEM_CLASS))[4]; 288 | 289 | expect(formComponent.form.get('ratingControl').disabled).toBeFalsy(); 290 | 291 | formComponent.form.disable(); 292 | formFixture.detectChanges(); 293 | 294 | expect(formComponent.form.get('ratingControl').disabled).toBeTruthy(); 295 | ratingItemDebugElement.triggerEventHandler('click', null); 296 | formFixture.detectChanges(); 297 | 298 | expect(formComponent.ratingComponent._selectedIndex).toBe(4); 299 | }); 300 | }); 301 | 302 | describe('ng rating keyboard support', () => { 303 | let keyboardfixture: ComponentFixture; 304 | let keyboardComponent: NgRatingTestComponent; 305 | 306 | beforeEach(() => { 307 | keyboardfixture = TestBed.createComponent(NgRatingTestComponent); 308 | keyboardComponent = keyboardfixture.componentInstance; 309 | keyboardfixture.detectChanges(); 310 | }); 311 | 312 | it('should handle arrow keys', () => { 313 | const element = keyboardfixture.debugElement.query(By.directive(NgRatingComponent)); 314 | let event = createKeyDownEvent(Key.ArrowRight); 315 | keyboardComponent.ratingComponent.handleKeyDown(event); 316 | keyboardfixture.detectChanges(); 317 | 318 | expect(keyboardComponent.ratingLabel).toEqual(1); 319 | expect(keyboardComponent.ratingComponent._selectedIndex).toBe(0); 320 | expect(element.nativeElement.getAttribute('aria-valuenow')).toEqual('1'); 321 | expect(event.preventDefault).toHaveBeenCalled(); 322 | 323 | event = createKeyDownEvent(Key.ArrowUp); 324 | keyboardComponent.ratingComponent.handleKeyDown(event); 325 | keyboardfixture.detectChanges(); 326 | 327 | expect(keyboardComponent.ratingLabel).toEqual(2); 328 | expect(keyboardComponent.ratingComponent._selectedIndex).toBe(1); 329 | expect(element.nativeElement.getAttribute('aria-valuenow')).toEqual('2'); 330 | expect(event.preventDefault).toHaveBeenCalled(); 331 | 332 | event = createKeyDownEvent(Key.ArrowLeft); 333 | keyboardComponent.ratingComponent.handleKeyDown(event); 334 | keyboardfixture.detectChanges(); 335 | 336 | expect(keyboardComponent.ratingLabel).toEqual(1); 337 | expect(keyboardComponent.ratingComponent._selectedIndex).toBe(0); 338 | expect(element.nativeElement.getAttribute('aria-valuenow')).toEqual('1'); 339 | expect(event.preventDefault).toHaveBeenCalled(); 340 | 341 | event = createKeyDownEvent(Key.ArrowDown); 342 | keyboardComponent.ratingComponent.handleKeyDown(event); 343 | keyboardfixture.detectChanges(); 344 | 345 | expect(keyboardComponent.ratingLabel).toEqual(0); 346 | expect(keyboardComponent.ratingComponent._selectedIndex).toBe(-1); 347 | expect(element.nativeElement.getAttribute('aria-valuenow')).toEqual('0'); 348 | expect(event.preventDefault).toHaveBeenCalled(); 349 | }); 350 | 351 | it('should handle home/end keys', () => { 352 | const element = keyboardfixture.debugElement.query(By.directive(NgRatingComponent)); 353 | let event = createKeyDownEvent(Key.Home); 354 | keyboardComponent.ratingComponent.handleKeyDown(event); 355 | keyboardfixture.detectChanges(); 356 | 357 | expect(keyboardComponent.ratingLabel).toEqual(1); 358 | expect(keyboardComponent.ratingComponent._selectedIndex).toBe(0); 359 | expect(element.nativeElement.getAttribute('aria-valuenow')).toEqual('1'); 360 | expect(event.preventDefault).toHaveBeenCalled(); 361 | 362 | event = createKeyDownEvent(Key.End); 363 | keyboardComponent.ratingComponent.handleKeyDown(event); 364 | keyboardfixture.detectChanges(); 365 | 366 | expect(keyboardComponent.ratingLabel).toEqual(keyboardComponent.size); 367 | expect(keyboardComponent.ratingComponent._selectedIndex).toBe(keyboardComponent.size - 1); 368 | expect(element.nativeElement.getAttribute('aria-valuenow')).toEqual(`${keyboardComponent.size}`); 369 | expect(event.preventDefault).toHaveBeenCalled(); 370 | }); 371 | }); 372 | }); 373 | 374 | function createKeyDownEvent(key: string): KeyboardEvent { 375 | const event = { code: key, preventDefault: () => {} }; 376 | spyOn(event, 'preventDefault'); 377 | return event as KeyboardEvent; 378 | } 379 | 380 | @Component({ 381 | template: ` 382 | 383 | {{ this.ratingLabel }} 384 | 385 | `, 386 | }) 387 | export class NgRatingTestComponent { 388 | @ViewChild(NgRatingComponent) ratingComponent: NgRatingComponent; 389 | public ratingLabel: number; 390 | public size = 6; 391 | public showCancelIcon: boolean; 392 | } 393 | 394 | @Component({ 395 | template: ` `, 396 | }) 397 | export class NgRatingPreDefinedTestComponent { 398 | @ViewChild(NgRatingComponent) ratingComponent: NgRatingComponent; 399 | public size = 6; 400 | public rating = 3; 401 | } 402 | 403 | @Component({ 404 | template: ` 405 | 406 | `, 407 | }) 408 | export class NgRatingReadonlyTestComponent { 409 | @ViewChild(NgRatingComponent) ratingComponent: NgRatingComponent; 410 | public size = 6; 411 | public rating = 3; 412 | public readonly = false; 413 | public disabled = false; 414 | } 415 | 416 | @Component({ 417 | template: ` `, 418 | }) 419 | export class NgRatingControlValueAccessorTestComponent { 420 | @ViewChild(NgRatingComponent) ratingComponent: NgRatingComponent; 421 | public size = 6; 422 | public rating: number; 423 | } 424 | 425 | @Component({ 426 | template: ` 427 |
428 | 429 |
430 | `, 431 | }) 432 | export class NgRatingFormControlTestComponent implements OnInit { 433 | @ViewChild(NgRatingComponent) ratingComponent: NgRatingComponent; 434 | public size = 6; 435 | public form: FormGroup; 436 | 437 | constructor(private formBuilder: FormBuilder) {} 438 | 439 | ngOnInit(): void { 440 | this.form = this.formBuilder.group({ 441 | ratingControl: [3, Validators.required], 442 | }); 443 | } 444 | } 445 | --------------------------------------------------------------------------------