├── projects ├── ngx-bar-rating-demo │ ├── src │ │ ├── app │ │ │ ├── bars │ │ │ │ ├── bars.component.scss │ │ │ │ ├── bars.component.ts │ │ │ │ └── bars.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.html │ │ │ ├── header │ │ │ │ ├── header.component.scss │ │ │ │ ├── header.component.html │ │ │ │ └── header.component.ts │ │ │ ├── app.config.ts │ │ │ ├── app.config.server.ts │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.ts │ │ │ │ └── footer.component.scss │ │ │ ├── stars │ │ │ │ ├── stars.component.scss │ │ │ │ ├── stars.component.ts │ │ │ │ └── stars.component.html │ │ │ └── app.component.ts │ │ ├── main.ts │ │ ├── main.server.ts │ │ ├── server.ts │ │ ├── styles.scss │ │ └── index.html │ ├── public │ │ ├── favicon.ico │ │ ├── screenshot.png │ │ ├── mstile-70x70.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ ├── site.webmanifest │ │ ├── br-pokemon-theme.scss │ │ ├── ngx-bar-rating.svg │ │ ├── safari-pinned-tab.svg │ │ └── custom │ │ │ ├── 003-bellsprout.svg │ │ │ ├── 004-squirtle.svg │ │ │ ├── 007-pikachu.svg │ │ │ ├── 001-caterpie.svg │ │ │ ├── 006-bullbasaur.svg │ │ │ ├── 002-venonat.svg │ │ │ ├── 008-charmander.svg │ │ │ └── 005-jigglypuff.svg │ ├── tsconfig.spec.json │ └── tsconfig.app.json └── ngx-bar-rating │ ├── src │ ├── public-api.ts │ ├── lib │ │ ├── custom-rating.ts │ │ ├── bar-rating.module.ts │ │ ├── bar-rating.model.ts │ │ ├── bar-rating.scss │ │ ├── bar-rating.html │ │ ├── bar-rating-effect.ts │ │ ├── custom-rating.spec.ts │ │ ├── bar-rating-options.spec.ts │ │ ├── bar-rating-effect.spec.ts │ │ ├── bar-rating.ts │ │ └── bar-rating.spec.ts │ └── test.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── themes │ ├── br-default-theme.scss │ ├── br-vertical-theme.scss │ ├── br-movie-theme.scss │ ├── br-horizontal-theme.scss │ ├── br-square-theme.scss │ └── br-stars-theme.scss │ ├── package.json │ ├── .eslintrc.json │ ├── karma.conf.js │ └── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature.md │ └── bug_report.md ├── FUNDING.md └── workflows │ ├── netlify.yml │ └── integrate.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── tsconfig.json ├── .eslintrc.json ├── package.json ├── CHANGELOG.md ├── angular.json └── README.md /projects/ngx-bar-rating-demo/src/app/bars/bars.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | padding-top: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/favicon.ico -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/screenshot.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/mstile-70x70.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/favicon-16x16.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/favicon-32x32.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/mstile-144x144.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/mstile-150x150.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/mstile-310x150.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/mstile-310x310.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/apple-touch-icon.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MurhafSousli/ngx-bar-rating/HEAD/projects/ngx-bar-rating-demo/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/bar-rating'; 2 | export * from './lib/bar-rating.model'; 3 | export * from './lib/bar-rating.module'; 4 | export * from './lib/custom-rating'; 5 | export * from './lib/bar-rating-effect'; 6 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-bar-rating", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "assets": [ 8 | "./themes/*.scss" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | import { config } from './app/app.config.server'; 4 | 5 | const bootstrap = () => bootstrapApplication(AppComponent, config); 6 | 7 | export default bootstrap; 8 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffc40d 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Propose a new feature for ngx-bar-rating 4 | labels: feature 5 | --- 6 | 7 | #### Feature Description 8 | 9 | Provide a brief summary of the feature you would like to see. 10 | 11 | #### Use Case 12 | 13 | Describe the use case(s) that the proposed feature would enable. 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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | text-align: center; 3 | } 4 | 5 | .forkme { 6 | position: fixed; 7 | left: 2em; 8 | top: 1.5em; 9 | z-index: 99999; 10 | opacity: 0.4; 11 | 12 | i { 13 | font-size: 60px; 14 | } 15 | } 16 | 17 | img { 18 | margin-bottom: 30px; 19 | width: 200px; 20 | } 21 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |

Angular Bar Rating

7 |

Minimal, light-weight Angular ratings.

8 |
9 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; 3 | 4 | export const appConfig: ApplicationConfig = { 5 | providers: [ 6 | provideZoneChangeDetection({ eventCoalescing: true }), 7 | provideClientHydration(withEventReplay()) 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; 2 | import { provideServerRendering } from '@angular/platform-server'; 3 | import { appConfig } from './app.config'; 4 | 5 | const serverConfig: ApplicationConfig = { 6 | providers: [ 7 | provideServerRendering(), 8 | ] 9 | }; 10 | 11 | export const config = mergeApplicationConfig(appConfig, serverConfig); 12 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 3 | 4 | @Component({ 5 | selector: 'app-footer', 6 | templateUrl: './footer.component.html', 7 | styleUrl: './footer.component.scss', 8 | imports: [FaIconComponent], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class FooterComponent { 12 | } 13 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | templateUrl: './header.component.html', 7 | styleUrl: './header.component.scss', 8 | imports: [FaIconComponent], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class HeaderComponent { 12 | } 13 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/stars/stars.component.scss: -------------------------------------------------------------------------------- 1 | section { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | margin: 3em 0; 7 | } 8 | 9 | .stars { 10 | margin-top: 100px; 11 | } 12 | 13 | .link { 14 | margin-left: 5px; 15 | cursor: pointer; 16 | text-decoration: none; 17 | color: #50e3c2; 18 | } 19 | 20 | .star-wrapper { 21 | height: 37px; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | 4 | .author { 5 | margin-top: -0.5em; 6 | } 7 | 8 | .author-links { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | font-size: 2em; 13 | padding: 1em 0 2em; 14 | 15 | a { 16 | color: #757575; 17 | text-decoration: none; 18 | margin: 0.5em; 19 | opacity: 0.5; 20 | 21 | &:hover { 22 | opacity: 1; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /.github/FUNDING.md: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: murhafsousli 4 | patreon: murhaf 5 | ko_fi: # Replace with a single Ko-fi username 6 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | liberapay: # Replace with a single Liberapay username 9 | issuehunt: # Replace with a single IssueHunt username 10 | otechie: # Replace with a single Otechie username 11 | custom: # Replace with a single custom sponsorship URL 12 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/custom-rating.ts: -------------------------------------------------------------------------------- 1 | import { Directive, inject, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[ratingActive]' 5 | }) 6 | export class ActiveRating { 7 | template: TemplateRef = inject(TemplateRef); 8 | } 9 | 10 | @Directive({ 11 | selector: '[ratingInactive]' 12 | }) 13 | export class InactiveRating { 14 | template: TemplateRef = inject(TemplateRef); 15 | } 16 | 17 | @Directive({ 18 | selector: '[ratingFraction]' 19 | }) 20 | export class FractionRating { 21 | template: TemplateRef = inject(TemplateRef); 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug found in ngx-bar-rating 4 | --- 5 | 6 | 7 | #### Reproduction 8 | 9 | Use StackBlitz to reproduce your issue: https://stackblitz.com/edit/ngx-bar-rating 10 | 11 | Steps to reproduce: 12 | 1. 13 | 2. 14 | 15 | 16 | #### Expected Behavior 17 | 18 | What behavior were you expecting to see? 19 | 20 | 21 | #### Actual Behavior 22 | 23 | What behavior did you actually see? 24 | 25 | 26 | #### Environment 27 | 28 | - Angular: 29 | - ngx-bar-rating: 30 | - Browser(s): 31 | - Operating System (e.g. Windows, macOS, Ubuntu): 32 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/app", 7 | "types": [ 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/main.ts", 13 | "src/main.server.ts", 14 | "src/server.ts" 15 | ], 16 | "include": [ 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /projects/ngx-bar-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 { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting(), { 15 | teardown: { destroyAfterEach: false } 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BarRating } from './bar-rating'; 3 | import { ActiveRating, InactiveRating, FractionRating } from './custom-rating'; 4 | import { BarRatingEffect } from './bar-rating-effect'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | BarRating, 9 | BarRatingEffect, 10 | ActiveRating, 11 | InactiveRating, 12 | FractionRating 13 | ], 14 | exports: [ 15 | BarRating, 16 | BarRatingEffect, 17 | ActiveRating, 18 | InactiveRating, 19 | FractionRating 20 | ] 21 | }) 22 | export class BarRatingModule { 23 | } 24 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/bars/bars.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { BarRating, BarRatingEffect } from 'ngx-bar-rating'; 3 | 4 | @Component({ 5 | selector: 'bars', 6 | templateUrl: './bars.component.html', 7 | styleUrl: './bars.component.scss', 8 | imports: [BarRating, BarRatingEffect], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class BarsComponent { 12 | horiRate: number = 7; 13 | vertRate: number = 1; 14 | squareRate: number = 3; 15 | movieRate: number = 2; 16 | customRate: number = 2; 17 | starRate: number = 4; 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/themes/br-default-theme.scss: -------------------------------------------------------------------------------- 1 | $star-utf8: '\2605'; 2 | 3 | .br-default { 4 | --_br-font-size: 26px; 5 | 6 | .br-units { 7 | align-items: center; 8 | } 9 | 10 | .br-unit-inner { 11 | 12 | &:before { 13 | content: $star-utf8; 14 | color: var(--_br-inactive-color); 15 | width: 100%; 16 | } 17 | } 18 | 19 | .br-active:before { 20 | color: var(--_br-active-color); 21 | } 22 | 23 | .br-fraction { 24 | &:after { 25 | color: var(--_br-active-color); 26 | content: $star-utf8; 27 | width: 50%; 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | overflow: hidden; 32 | } 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.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 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/themes/br-vertical-theme.scss: -------------------------------------------------------------------------------- 1 | .br-vertical { 2 | --br-width: 120px; 3 | --br-height: 5px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | 8 | .br-units { 9 | width: 120px; 10 | flex-direction: column-reverse; 11 | margin: 10px 0; 12 | } 13 | 14 | .br-unit-inner { 15 | display: block; 16 | width: var(--br-width); 17 | height: var(--br-height); 18 | background-color: var(--_br-active-color); 19 | margin-top: 1px; 20 | opacity: 0.25; 21 | 22 | &.br-active { 23 | opacity: 1; 24 | } 25 | } 26 | 27 | &.br-readonly { 28 | 29 | .br-active { 30 | opacity: 0.1; 31 | } 32 | } 33 | 34 | .br-text { 35 | font-size: 18px; 36 | font-weight: 600; 37 | color: var(--_br-active-color); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/themes/br-movie-theme.scss: -------------------------------------------------------------------------------- 1 | .br-movie { 2 | --_br-active-color: #4278F5; 3 | --_br-gap: 2px; 4 | --br-width: 60px; 5 | --br-height: 8px; 6 | 7 | .br-units { 8 | margin: 15px 0; 9 | } 10 | 11 | .br-unit-inner { 12 | display: block; 13 | width: var(--br-width); 14 | height: var(--br-height); 15 | float: left; 16 | background-color: var(--_br-active-color); 17 | opacity: 0.25; 18 | 19 | &.br-active { 20 | opacity: 1; 21 | } 22 | } 23 | 24 | &.br-readonly { 25 | .br-active { 26 | opacity: 0.1; 27 | } 28 | } 29 | 30 | .br-text { 31 | letter-spacing: 2px; 32 | font-size: 16px; 33 | clear: both; 34 | text-align: center; 35 | font-weight: 600; 36 | display: block; 37 | color: var(--_br-active-color); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating.model.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, Provider } from '@angular/core'; 2 | 3 | export interface BarRatingOptions { 4 | theme?: string; 5 | maxValue?: number; 6 | showText?: boolean; 7 | readonly?: boolean; 8 | } 9 | 10 | const defaultOptions: BarRatingOptions = { 11 | theme: 'default', 12 | maxValue: 5, 13 | showText: false, 14 | readonly: false 15 | }; 16 | 17 | export const BAR_RATING_OPTIONS: InjectionToken = new InjectionToken('BAR_RATING_OPTIONS', { 18 | providedIn: 'root', 19 | factory: (): BarRatingOptions => defaultOptions 20 | }); 21 | 22 | export function provideBarRatingOptions(options: BarRatingOptions): Provider { 23 | return { 24 | provide: BAR_RATING_OPTIONS, 25 | useValue: { ...defaultOptions, ...options } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-bar-rating", 3 | "version": "8.0.1", 4 | "description": "Angular Bar Rating", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/MurhafSousli/ngx-bar-rating.git" 8 | }, 9 | "homepage": "https://github.com/MurhafSousli/ngx-bar-rating#readme", 10 | "bugs": { 11 | "url": "https://github.com/MurhafSousli/ngx-bar-rating/issues" 12 | }, 13 | "keywords": [ 14 | "rating", 15 | "angular", 16 | "bar", 17 | "bar-rating", 18 | "star", 19 | "stars" 20 | ], 21 | "author": "Murhaf Sousli ", 22 | "license": "MIT", 23 | "peerDependencies": { 24 | "@angular/common": ">=19.0.0", 25 | "@angular/core": ">=19.0.0", 26 | "@angular/forms": ">=19.0.0" 27 | }, 28 | "dependencies": { 29 | "tslib": "^2.3.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/themes/br-horizontal-theme.scss: -------------------------------------------------------------------------------- 1 | .br-horizontal { 2 | --br-width: 12px; 3 | --br-height: 32px; 4 | 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | 9 | .br-units { 10 | } 11 | 12 | .br-unit-inner { 13 | display: block; 14 | width: var(--br-width); 15 | height: var(--br-height); 16 | float: left; 17 | background-color: var(--_br-active-color); 18 | margin: 1px; 19 | text-align: center; 20 | opacity: 0.25; 21 | 22 | &.br-active { 23 | opacity: 1; 24 | } 25 | } 26 | 27 | &.br-readonly { 28 | .br-unit-inner { 29 | cursor: default; 30 | 31 | &.br-active { 32 | opacity: 0.1; 33 | } 34 | } 35 | } 36 | 37 | .br-text { 38 | width: 20px; 39 | margin-left: 20px; 40 | font-size: 18px; 41 | color: var(--_br-active-color); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/stars/stars.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 3 | import { BarRatingEffect, BarRatingModule } from 'ngx-bar-rating'; 4 | 5 | @Component({ 6 | selector: 'stars', 7 | templateUrl: './stars.component.html', 8 | styleUrl: './stars.component.scss', 9 | imports: [ 10 | FaIconComponent, 11 | BarRatingModule, 12 | BarRatingEffect 13 | ], 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class StarsComponent { 17 | 18 | bootRate: number = 1; 19 | faRate: number = 1; 20 | cssRate: number = 1.6; 21 | faoRate: number = 5.6; 22 | faoRated: boolean = false; 23 | 24 | onFaoRate(e): void { 25 | this.faoRated = true; 26 | this.faoRate = e; 27 | } 28 | 29 | faoReset(): void { 30 | this.faoRated = false; 31 | this.faoRate = 5.6; 32 | } 33 | 34 | test(): void { 35 | console.log('test'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2025 Murhaf Sousli 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. 22 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/themes/br-square-theme.scss: -------------------------------------------------------------------------------- 1 | .br-square { 2 | --_br-active-color: #4278F5; 3 | --_br-font-size: 30px; 4 | --br-width: var(--_br-font-size); 5 | --br-height: var(--_br-font-size); 6 | 7 | .br-units { 8 | counter-reset: counter; 9 | } 10 | 11 | .br-unit-clone { 12 | .br-unit-inner { 13 | background-color: transparent; 14 | 15 | &:after { 16 | content: none !important; 17 | } 18 | } 19 | } 20 | 21 | .br-unit-inner { 22 | width: var(--br-width); 23 | height: var(--br-height); 24 | border: 2px solid var(--_br-active-color); 25 | background-color: white; 26 | margin: 2px; 27 | text-decoration: none; 28 | font-size: 14px; 29 | line-height: 2; 30 | text-align: center; 31 | color: var(--_br-active-color); 32 | font-weight: 600; 33 | opacity: 0.25; 34 | 35 | &.br-active { 36 | opacity: 1; 37 | } 38 | 39 | &:after { 40 | content: counter(counter); 41 | counter-increment: counter; 42 | } 43 | } 44 | 45 | &.br-readonly { 46 | 47 | .br-active { 48 | opacity: 0.1; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": false, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "paths": { 14 | "ngx-bar-rating": [ 15 | "./projects/ngx-bar-rating/src/public-api" 16 | ] 17 | }, 18 | "esModuleInterop": true, 19 | "sourceMap": true, 20 | "declaration": false, 21 | "experimentalDecorators": true, 22 | "moduleResolution": "node", 23 | "importHelpers": true, 24 | "target": "ES2022", 25 | "module": "ES2022", 26 | "useDefineForClassFields": false, 27 | "lib": [ 28 | "ES2022", 29 | "dom" 30 | ] 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:@angular-eslint/recommended", 15 | "plugin:@angular-eslint/template/process-inline-templates" 16 | ], 17 | "rules": { 18 | "@angular-eslint/directive-selector": [ 19 | "error", 20 | { 21 | "type": "attribute", 22 | "prefix": "app", 23 | "style": "camelCase" 24 | } 25 | ], 26 | "@angular-eslint/component-selector": [ 27 | "error", 28 | { 29 | "type": "element", 30 | "prefix": "app", 31 | "style": "kebab-case" 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "files": [ 38 | "*.html" 39 | ], 40 | "extends": [ 41 | "plugin:@angular-eslint/template/recommended", 42 | "plugin:@angular-eslint/template/accessibility" 43 | ], 44 | "rules": {} 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; 3 | import { faGithub, faGithubAlt, faXTwitter } from '@fortawesome/free-brands-svg-icons'; 4 | import { faStar, faStarHalfAlt, faTimesCircle } from '@fortawesome/free-solid-svg-icons'; 5 | import { faStar as farStar } from '@fortawesome/free-regular-svg-icons'; 6 | import { HeaderComponent } from './header/header.component'; 7 | import { BarsComponent } from './bars/bars.component'; 8 | import { StarsComponent } from './stars/stars.component'; 9 | import { FooterComponent } from './footer/footer.component'; 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | templateUrl: './app.component.html', 14 | styleUrl: './app.component.scss', 15 | imports: [ 16 | HeaderComponent, 17 | BarsComponent, 18 | StarsComponent, 19 | FooterComponent 20 | ] 21 | }) 22 | export class AppComponent { 23 | constructor(library: FaIconLibrary) { 24 | library.addIcons( 25 | faXTwitter, 26 | faGithub, 27 | faGithubAlt, 28 | faStar, 29 | faStarHalfAlt, 30 | farStar, 31 | faTimesCircle 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/netlify.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Demo 2 | 3 | on: 4 | push: 5 | branches: [ deploy-netlify ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Use Node.js 20 13 | uses: actions/setup-node@master 14 | with: 15 | node-version: 20 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Build demo 19 | run: npm run build 20 | env: 21 | GIST_CLIENT_ID: ${{ secrets.GIST_CLIENT_ID }} 22 | GIST_CLIENT_SECRET: ${{ secrets.GIST_CLIENT_SECRET }} 23 | - name: Deploy to Netlify 24 | uses: nwtgck/actions-netlify@master 25 | with: 26 | publish-dir: './dist/ngx-bar-rating-demo/browser' 27 | production-branch: deploy-netlify 28 | github-token: ${{ secrets.GITHUB_TOKEN }} 29 | deploy-message: "Deploy from GitHub Actions" 30 | enable-pull-request-comment: true 31 | enable-commit-comment: true 32 | overwrites-pull-request-comment: true 33 | env: 34 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 35 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 36 | timeout-minutes: 1 37 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :host { 6 | --_br-font-size: var(--br-font-size, 16px); 7 | --_br-gap: var(--br-gap, 0); 8 | --_br-active-color: var(--br-active-color, #EDB867); 9 | --_br-inactive-color: var(--br-inactive-color, #D2D2D2); 10 | 11 | --_br-effect-scale: var(--br-effect-scale, 1.5); 12 | --_br-effect-duration: var(--br-effect-duration, 0.4s); 13 | --_br-effect-ease: var(--br-effect-ease, ease-out); 14 | } 15 | 16 | .br { 17 | position: relative; 18 | } 19 | 20 | .br-units { 21 | display: flex; 22 | flex-wrap: nowrap; 23 | gap: var(--_br-gap); 24 | } 25 | 26 | .br-unit { 27 | font-size: var(--_br-font-size); 28 | cursor: pointer; 29 | -webkit-font-smoothing: antialiased; 30 | text-rendering: auto; 31 | } 32 | 33 | .br-unit-inner { 34 | position: relative; 35 | } 36 | 37 | .br-readonly, .br-disabled { 38 | .br-unit { 39 | cursor: default; 40 | pointer-events: none; 41 | } 42 | } 43 | 44 | .br-unit-clone { 45 | position: absolute; 46 | pointer-events: none; 47 | transform-origin: center; 48 | animation: scale-fade-out var(--_br-effect-duration) var(--_br-effect-ease); 49 | } 50 | 51 | @keyframes scale-fade-out { 52 | 0% { 53 | transform: scale(1); 54 | opacity: 1; 55 | } 56 | 100% { 57 | transform: scale(var(--_br-effect-scale)); 58 | opacity: 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/br-pokemon-theme.scss: -------------------------------------------------------------------------------- 1 | .br-pokemon { 2 | 3 | .br-unit { 4 | .br-unit-inner { 5 | background-position: center center; 6 | background-size: contain; 7 | background-repeat: no-repeat; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | 14 | margin-left: 5px; 15 | width: 40px; 16 | height: 40px; 17 | filter: grayscale(1); 18 | } 19 | 20 | &:first-child .br-unit-inner { 21 | margin-left: 0; 22 | background-image: url('custom/007-pikachu.svg'); 23 | } 24 | 25 | &:nth-child(2) .br-unit-inner { 26 | background-image: url('custom/006-bullbasaur.svg'); 27 | } 28 | 29 | &:nth-child(3) .br-unit-inner { 30 | background-image: url('custom/008-charmander.svg'); 31 | } 32 | 33 | &:nth-child(4) .br-unit-inner { 34 | background-image: url('custom/002-venonat.svg'); 35 | } 36 | 37 | &:nth-child(5) .br-unit-inner { 38 | background-image: url('custom/005-jigglypuff.svg'); 39 | } 40 | 41 | &:nth-child(6) .br-unit-inner { 42 | background-image: url('custom/001-caterpie.svg'); 43 | } 44 | 45 | // &:nth-child(4) { 46 | // background-image: url('/./custom/004-squirtle.svg'); 47 | // } 48 | // &:nth-child(5) { 49 | // background-image: url('/./custom/003-bellsprout.svg'); 50 | // } 51 | } 52 | 53 | .br-unit-inner.br-active { 54 | filter: grayscale(0); 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/ngx-bar-rating.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 14 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating.html: -------------------------------------------------------------------------------- 1 |
9 | 10 |
12 | @for (unit of contexts(); let i = $index; track i) { 13 | @let value = i + 1; 14 |
17 | @switch (unit) { 18 | @case (UNITS.fraction) { 19 | 20 | } 21 | @case (UNITS.inactive) { 22 | 23 | } 24 | @default { 25 | 26 | } 27 | } 28 |
29 | } 30 |
31 | 32 | @if (showText()) { 33 |
{{ ratingText() }}
34 | } 35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:@angular-eslint/recommended", 15 | "plugin:@angular-eslint/template/process-inline-templates" 16 | ], 17 | "rules": { 18 | "no-prototype-builtins": "off", 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "@angular-eslint/directive-selector": [ 21 | "error", 22 | { 23 | "type": "attribute", 24 | "prefix": "", 25 | "style": "camelCase" 26 | } 27 | ], 28 | "@angular-eslint/component-selector": [ 29 | "error", 30 | { 31 | "type": "element", 32 | "prefix": "", 33 | "style": "kebab-case" 34 | } 35 | ], 36 | "@angular-eslint/component-class-suffix": 0, 37 | "@angular-eslint/directive-class-suffix": 0, 38 | "@angular-eslint/no-input-rename": 0, 39 | "@angular-eslint/no-output-rename": 0, 40 | "@angular-eslint/no-output-native": 0, 41 | "@angular-eslint/no-host-metadata-property": 0 42 | } 43 | }, 44 | { 45 | "files": [ 46 | "*.html" 47 | ], 48 | "extends": [ 49 | "plugin:@angular-eslint/template/recommended", 50 | "plugin:@angular-eslint/template/accessibility" 51 | ], 52 | "rules": { 53 | "@angular-eslint/template/elements-content": 0 54 | } 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating-effect.ts: -------------------------------------------------------------------------------- 1 | import { Directive, inject, } from '@angular/core'; 2 | import { BarRating } from './bar-rating'; 3 | 4 | @Directive({ 5 | selector: 'bar-rating[effect]', 6 | }) 7 | export class BarRatingEffect { 8 | 9 | private barRating: BarRating = inject(BarRating, { 10 | host: true, 11 | self: true 12 | }); 13 | 14 | constructor() { 15 | // Save the original handleClick method 16 | const originalHandleClick = this.barRating.handleClick.bind(this.barRating); 17 | 18 | // Override the handleClick method 19 | this.barRating.handleClick = (event: MouseEvent) => { 20 | // Call the original handleClick logic 21 | originalHandleClick(event); 22 | 23 | // Add custom effect logic 24 | this.handleClickWithEffect(event); 25 | }; 26 | } 27 | 28 | handleClickWithEffect(event: MouseEvent): void { 29 | const target = event.target as HTMLElement; 30 | const unitElement = target.closest('.br-unit') as HTMLElement; 31 | 32 | const clone = unitElement.cloneNode(true) as HTMLElement; 33 | const rect: DOMRect = unitElement.getBoundingClientRect(); 34 | const parentRect: DOMRect = (event.currentTarget as HTMLElement).getBoundingClientRect(); 35 | 36 | clone.classList.add('br-unit-clone'); 37 | clone.style.left = `${ rect.left - parentRect.left }px`; 38 | clone.style.top = `${ rect.top - parentRect.top }px`; 39 | clone.style.width = `${ rect.width }px`; 40 | clone.style.height = `${ rect.height }px`; 41 | 42 | const parentElement = event.currentTarget as HTMLElement; 43 | parentElement.appendChild(clone); 44 | 45 | clone.addEventListener('animationend', () => { 46 | parentElement.removeChild(clone); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/ngx-bar-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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/ngx-bar-rating'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' }, 33 | { type: 'cobertura' }, 34 | { type: 'lcov' } 35 | ] 36 | }, 37 | reporters: ['progress', 'kjhtml'], 38 | browsers: ["MyChromeWithoutSearchSelect"], 39 | customLaunchers: { 40 | MyChromeWithoutSearchSelect: { 41 | base: "Chrome", 42 | flags: ["-disable-search-engine-choice-screen"], 43 | }, 44 | }, 45 | restartOnFileChange: true 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /.github/workflows/integrate.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | push: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | # Machine environment: 12 | # We specify the Node.js version manually below, and use versioned Chrome from Puppeteer. 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - name: Use Node.js 20 18 | uses: actions/setup-node@master 19 | with: 20 | node-version: 20 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build-lib 27 | 28 | - name: Lint 29 | run: npm run lint-lib 30 | 31 | - name: Test 32 | run: npm run test-lib-headless 33 | 34 | - name: Upload coverage reports to Codecov 35 | uses: codecov/codecov-action@main 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | slug: MurhafSousli/ngx-bar-rating 39 | 40 | - name: Code Coverage Report 41 | uses: irongut/CodeCoverageSummary@master 42 | with: 43 | filename: coverage/**/cobertura-coverage.xml 44 | badge: true 45 | fail_below_min: true 46 | format: markdown 47 | hide_branch_rate: false 48 | hide_complexity: true 49 | indicators: true 50 | output: both 51 | thresholds: '60 80' 52 | 53 | - name: Add Coverage PR Comment 54 | uses: marocchino/sticky-pull-request-comment@main 55 | if: github.event_name == 'pull_request' 56 | with: 57 | recreate: true 58 | path: code-coverage-results.md 59 | continue-on-error: true # Allow this step to fail 60 | 61 | - name: Build demo (ssr) 62 | run: npm run build-ssr 63 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/custom-rating.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { Component, Signal, viewChild } from '@angular/core'; 3 | import { NgTemplateOutlet } from '@angular/common'; 4 | import { BarRating, ActiveRating, InactiveRating, FractionRating, BarRatingModule } from 'ngx-bar-rating'; 5 | 6 | @Component({ 7 | template: ` 8 | 9 |
10 | Custom Active Template 11 |
12 |
13 | Custom Inactive Template 14 |
15 |
16 | Custom Fraction Template 17 |
18 |
19 | `, 20 | imports: [BarRatingModule] 21 | }) 22 | class TestHostComponent { 23 | barRating: Signal = viewChild.required(BarRating); 24 | } 25 | 26 | describe('BarRating Component with Custom Rating Templates', () => { 27 | let fixture: ComponentFixture; 28 | let component: TestHostComponent; 29 | 30 | beforeEach(async () => { 31 | await TestBed.configureTestingModule({ 32 | imports: [ 33 | BarRating, 34 | ActiveRating, 35 | InactiveRating, 36 | FractionRating, 37 | TestHostComponent, 38 | NgTemplateOutlet 39 | ] 40 | }).compileComponents(); 41 | 42 | fixture = TestBed.createComponent(TestHostComponent); 43 | component = fixture.componentInstance; 44 | fixture.detectChanges(); 45 | }); 46 | 47 | it('should create the BarRating component', () => { 48 | expect(component.barRating()).toBeTruthy(); 49 | expect(component.barRating().customActiveRating()).toBeTruthy(); 50 | expect(component.barRating().customInActiveRating()).toBeTruthy(); 51 | expect(component.barRating().customFractionRating()).toBeTruthy(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/bars/bars.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
Horizontal Rating
7 |
8 | 9 |
10 |
11 | 12 |
13 |
Square Rating
14 |
15 | 16 |
17 |
18 | 19 |
20 |
Custom Rating
21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 |
Movie Rating
32 |
33 | 39 |
40 |
41 | 42 |
43 |
Star Rating
44 |
45 | 46 |
47 |
48 | 49 |
50 |
Vertical Rating
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating-options.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { BAR_RATING_OPTIONS, provideBarRatingOptions, BarRatingOptions } from 'ngx-bar-rating'; 3 | 4 | describe('BarRating Global Options', () => { 5 | const defaultOptions: BarRatingOptions = { 6 | theme: 'default', 7 | maxValue: 5, 8 | showText: false, 9 | readonly: false, 10 | }; 11 | 12 | it('should provide default options via BAR_RATING_OPTIONS token', () => { 13 | const options: BarRatingOptions = TestBed.inject(BAR_RATING_OPTIONS); 14 | expect(options).toEqual(defaultOptions); 15 | }); 16 | 17 | it('should override default options with custom values', () => { 18 | const customOptions: BarRatingOptions = { 19 | theme: 'custom-theme', 20 | maxValue: 10, 21 | showText: true, 22 | readonly: true, 23 | }; 24 | 25 | TestBed.configureTestingModule({ 26 | providers: [provideBarRatingOptions(customOptions)], 27 | }); 28 | 29 | const options = TestBed.inject(BAR_RATING_OPTIONS); 30 | expect(options).toEqual({ 31 | ...defaultOptions, 32 | ...customOptions, 33 | }); 34 | }); 35 | 36 | it('should merge default options with partially provided custom values', () => { 37 | const customOptions: Partial = { 38 | theme: 'custom-theme', 39 | }; 40 | 41 | TestBed.configureTestingModule({ 42 | providers: [provideBarRatingOptions(customOptions as BarRatingOptions)], 43 | }); 44 | 45 | const options = TestBed.inject(BAR_RATING_OPTIONS); 46 | expect(options).toEqual({ 47 | theme: 'custom-theme', 48 | maxValue: 5, 49 | showText: false, 50 | readonly: false, 51 | }); 52 | }); 53 | 54 | it('should provide options in root scope via factory', () => { 55 | const options: BarRatingOptions = TestBed.inject(BAR_RATING_OPTIONS); 56 | expect(options).toBeTruthy(); 57 | expect(options.theme).toBe('default'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/themes/br-stars-theme.scss: -------------------------------------------------------------------------------- 1 | .br-stars { 2 | --_br-gap: 5px; 3 | --_br-font-size: 28px; 4 | --br-width: var(--_br-font-size); 5 | --br-height: var(--_br-font-size); 6 | 7 | .br-unit-inner { 8 | background-position: center center; 9 | background-size: contain; 10 | background-repeat: no-repeat; 11 | background-image: url("data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='iso-8859-1'?%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512.002 512.002' style='enable-background:new 0 0 512.002 512.002;' xml:space='preserve'%3E%3Cpath style='fill:%23F0D355;' d='M400.07,502.64c-3.551,0-7.116-0.825-10.398-2.5L256.001,431.95L122.33,500.14 c-3.283,1.675-6.848,2.5-10.399,2.5c-4.728,0-9.431-1.464-13.408-4.336c-6.961-5.031-10.52-13.537-9.215-22.026l22.995-149.648 L6.579,219.236c-6.002-6.096-8.103-15.031-5.446-23.163c2.656-8.131,9.626-14.103,18.069-15.481l147.912-24.138l68.49-134.585 c3.904-7.675,11.786-12.507,20.397-12.507c8.611,0,16.493,4.833,20.396,12.507l68.49,134.585L492.8,180.592 c8.444,1.378,15.413,7.35,18.07,15.481c2.656,8.132,0.555,17.067-5.447,23.163L399.7,326.628l22.993,149.648 c1.305,8.49-2.254,16.995-9.215,22.026C409.502,501.175,404.798,502.64,400.07,502.64z'/%3E%3Cg style='opacity:0.1;'%3E%3Cpath style='fill:%23414042;' d='M146.585,486.578c-6.96-5.03-10.52-13.537-9.215-22.026l22.995-149.647L54.643,207.512 c-6.003-6.096-8.103-15.031-5.447-23.163c1.232-3.769,3.397-7.065,6.199-9.663l-36.193,5.906 c-8.444,1.378-15.413,7.35-18.069,15.481c-2.657,8.132-0.556,17.067,5.446,23.163l105.724,107.391L89.307,476.276 c-1.305,8.49,2.254,16.995,9.215,22.026c3.977,2.872,8.68,4.336,13.408,4.336c3.551,0,7.117-0.826,10.399-2.5l25.267-12.89 C147.257,487.03,146.914,486.817,146.585,486.578z'/%3E%3C/g%3E%3C/svg%3E"); 12 | width: var(--br-width); 13 | height: var(--br-height); 14 | filter: grayscale(1); 15 | 16 | &.br-active { 17 | filter: grayscale(0); 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/server.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { CommonEngine, isMainModule } from '@angular/ssr/node'; 3 | import express from 'express'; 4 | import { dirname, join, resolve } from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | import bootstrap from './main.server'; 7 | 8 | const serverDistFolder = dirname(fileURLToPath(import.meta.url)); 9 | const browserDistFolder = resolve(serverDistFolder, '../browser'); 10 | const indexHtml = join(serverDistFolder, 'index.server.html'); 11 | 12 | const app = express(); 13 | const commonEngine = new CommonEngine(); 14 | 15 | /** 16 | * Example Express Rest API endpoints can be defined here. 17 | * Uncomment and define endpoints as necessary. 18 | * 19 | * Example: 20 | * ```ts 21 | * app.get('/api/**', (req, res) => { 22 | * // Handle API request 23 | * }); 24 | * ``` 25 | */ 26 | 27 | /** 28 | * Serve static files from /browser 29 | */ 30 | app.get( 31 | '**', 32 | express.static(browserDistFolder, { 33 | maxAge: '1y', 34 | index: 'index.html' 35 | }), 36 | ); 37 | 38 | /** 39 | * Handle all other requests by rendering the Angular application. 40 | */ 41 | app.get('**', (req, res, next) => { 42 | const { protocol, originalUrl, baseUrl, headers } = req; 43 | 44 | commonEngine 45 | .render({ 46 | bootstrap, 47 | documentFilePath: indexHtml, 48 | url: `${protocol}://${headers.host}${originalUrl}`, 49 | publicPath: browserDistFolder, 50 | providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], 51 | }) 52 | .then((html) => res.send(html)) 53 | .catch((err) => next(err)); 54 | }); 55 | 56 | /** 57 | * Start the server if this module is the main entry point. 58 | * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. 59 | */ 60 | if (isMainModule(import.meta.url)) { 61 | const port = process.env['PORT'] || 4000; 62 | app.listen(port, () => { 63 | console.log(`Node Express server listening on http://localhost:${port}`); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../ngx-bar-rating/themes/br-stars-theme'; 2 | @use '../../ngx-bar-rating/themes/br-default-theme'; 3 | @use '../../ngx-bar-rating/themes/br-square-theme'; 4 | @use '../../ngx-bar-rating/themes/br-horizontal-theme'; 5 | @use '../../ngx-bar-rating/themes/br-vertical-theme'; 6 | @use '../../ngx-bar-rating/themes/br-movie-theme'; 7 | @use '../../ngx-bar-rating-demo/public/br-pokemon-theme'; 8 | 9 | $orange-color: #EDB867; 10 | $green-color: #50E3C2; 11 | $blue-color: #4278F5; 12 | 13 | $black: #252627; 14 | $gray: #757575; 15 | 16 | * { 17 | font-family: 'Poppins', sans-serif; 18 | } 19 | 20 | body { 21 | color: $black; 22 | } 23 | 24 | .main { 25 | margin-top: -1.4em; 26 | padding-top: 150px; 27 | padding-bottom: 100px; 28 | background-color: #F8F8F8; 29 | } 30 | 31 | .container { 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: center; 35 | align-items: center; 36 | max-width: 1140px; 37 | } 38 | 39 | .title { 40 | font-size: 4em; 41 | } 42 | 43 | .description { 44 | margin-top: 3em; 45 | color: $gray; 46 | } 47 | 48 | h2, h3, p, h4 { 49 | color: $gray; 50 | } 51 | 52 | h3 { 53 | margin-bottom: 2em; 54 | } 55 | 56 | .card { 57 | border-width: 2px; 58 | border-radius: 0; 59 | margin-bottom: 30px; 60 | } 61 | 62 | .card-heading { 63 | padding: 14px; 64 | text-align: center; 65 | font-size: 1.4em; 66 | border-radius: 0; 67 | font-family: 'Poppins', sans-serif; 68 | } 69 | 70 | .card-body { 71 | height: 150px; 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | } 76 | 77 | .row { 78 | width: 85%; 79 | } 80 | 81 | .card-primary { 82 | border-color: $blue-color; 83 | 84 | .card-heading { 85 | background-color: $blue-color; 86 | color: white; 87 | } 88 | } 89 | 90 | .card-warning { 91 | border-color: $orange-color; 92 | 93 | .card-heading { 94 | border-bottom: 1px solid transparent; 95 | background-color: $orange-color; 96 | color: white; 97 | } 98 | } 99 | 100 | .card-success { 101 | border-color: $green-color; 102 | 103 | .card-heading { 104 | border-bottom: 1px solid transparent; 105 | background-color: $green-color; 106 | color: white; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/app/stars/stars.component.html: -------------------------------------------------------------------------------- 1 |
2 |

A few flavours of star ratings compatible with popular libraries

3 |
4 | 5 |
6 |
7 |
8 | 9 |
10 |

11 |

CSS Stars

12 |
13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 |
23 |

24 |

Bootstrap

25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 |
36 |

37 |

Fontawesome

38 |
39 |
40 |
41 | 42 | 43 |
44 |

It can be used to display fractional star ratings.

45 | 46 | 47 | 48 | 49 | 50 | 51 |

52 | 53 | @if (faoRated) { 54 |

Your rating: {{ faoRate }} 55 | 56 |

57 | } @else { 58 |

Current rating: {{ faoRate }}

59 | } 60 |
61 |
62 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Bar Rating 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 47 | 48 |
58 | Loading... 59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-bar-rating-project", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "ng serve", 6 | "start-mobile": "ng serve --host 0.0.0.0", 7 | "build": "ng build ngx-bar-rating-demo --ssr=false --prerender=false", 8 | "build-ssr": "ng build ngx-bar-rating-demo", 9 | "watch": "ng build --watch --configuration development", 10 | "build-lib": "ng build ngx-bar-rating", 11 | "lint-lib": "ng lint ngx-bar-rating", 12 | "test-lib": "ng test ngx-bar-rating", 13 | "test-lib-headless": "ng test ngx-bar-rating --watch=false --no-progress --browsers=ChromeHeadless --code-coverage", 14 | "serve:ssr:ngx-bar-rating-demo": "node dist/ngx-bar-rating-demo/server/server.mjs" 15 | }, 16 | "private": true, 17 | "dependencies": { 18 | "@angular/animations": "^19.1.1", 19 | "@angular/common": "^19.1.1", 20 | "@angular/compiler": "^19.1.1", 21 | "@angular/core": "^19.1.1", 22 | "@angular/forms": "^19.1.1", 23 | "@angular/platform-browser": "^19.1.1", 24 | "@angular/platform-browser-dynamic": "^19.1.1", 25 | "@angular/platform-server": "^19.1.1", 26 | "@angular/router": "^19.1.1", 27 | "@angular/ssr": "^19.1.1", 28 | "@fortawesome/angular-fontawesome": "^1.0.0", 29 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 30 | "@fortawesome/free-brands-svg-icons": "^6.7.2", 31 | "@fortawesome/free-regular-svg-icons": "^6.7.2", 32 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 33 | "bootstrap": "^5.3.3", 34 | "bootstrap-icons": "^1.11.3", 35 | "rxjs": "~7.8.0", 36 | "zone.js": "^0.15.0" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "^19.1.1", 40 | "@angular-eslint/builder": "19.0.2", 41 | "@angular-eslint/eslint-plugin": "19.0.2", 42 | "@angular-eslint/eslint-plugin-template": "19.0.2", 43 | "@angular-eslint/schematics": "19.0.2", 44 | "@angular-eslint/template-parser": "19.0.2", 45 | "@angular/cli": "^19.1.1", 46 | "@angular/compiler-cli": "^19.1.1", 47 | "@types/express": "^4.17.17", 48 | "@types/jasmine": "~5.1.5", 49 | "@types/node": "^22.10.7", 50 | "@typescript-eslint/eslint-plugin": "^8.20.0", 51 | "@typescript-eslint/parser": "^8.20.0", 52 | "@typescript-eslint/types": "^8.20.0", 53 | "@typescript-eslint/utils": "^8.19.0", 54 | "eslint": "^9.18.0", 55 | "express": "^4.18.2", 56 | "jasmine-core": "^5.5.0", 57 | "karma": "~6.4.4", 58 | "karma-chrome-launcher": "~3.2.0", 59 | "karma-coverage": "~2.2.0", 60 | "karma-jasmine": "~5.1.0", 61 | "karma-jasmine-html-reporter": "~2.1.0", 62 | "ng-packagr": "^19.1.0", 63 | "tslib": "^2.7.0", 64 | "typescript": "~5.5.4" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating-effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { Component, DebugElement } from '@angular/core'; 3 | import { By } from '@angular/platform-browser'; 4 | import { BarRating, BarRatingEffect } from 'ngx-bar-rating'; 5 | 6 | @Component({ 7 | imports: [BarRating, BarRatingEffect], 8 | template: ` 9 | 10 | ` 11 | }) 12 | class TestHostComponent { 13 | } 14 | 15 | describe('BarRating Click Effect Directive', () => { 16 | let fixture: ComponentFixture; 17 | let debugElement: DebugElement; 18 | let directiveInstance: BarRatingEffect; 19 | let barRatingInstance: BarRating; 20 | 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ 23 | imports: [TestHostComponent], 24 | }).compileComponents(); 25 | 26 | fixture = TestBed.createComponent(TestHostComponent); 27 | debugElement = fixture.debugElement; 28 | fixture.detectChanges(); 29 | 30 | // Get the directive instance directly 31 | directiveInstance = debugElement.query(By.directive(BarRatingEffect)).injector.get(BarRatingEffect); 32 | barRatingInstance = debugElement.query(By.directive(BarRating)).componentInstance; 33 | }); 34 | 35 | it('should call both handleClick and handleClickWithEffect on click', () => { 36 | // Spy on both methods 37 | spyOn(barRatingInstance, 'handleClick').and.callThrough(); 38 | spyOn(directiveInstance, 'handleClickWithEffect').and.callThrough(); 39 | 40 | const event: MouseEvent = new MouseEvent('click', { bubbles: true }); 41 | const unitElement: DebugElement = debugElement.query(By.css('.br-unit')); 42 | 43 | // Simulate the click event on the .br-unit element inside BarRating 44 | unitElement.nativeElement.dispatchEvent(event); 45 | fixture.detectChanges(); 46 | 47 | // Check if both handleClick and handleClickWithEffect were called 48 | expect(barRatingInstance.handleClick).toHaveBeenCalled(); 49 | expect(directiveInstance.handleClickWithEffect).toHaveBeenCalledWith(event); 50 | }); 51 | 52 | it('should create and append a .br-unit-clone element on click', () => { 53 | const barRatingElement: HTMLElement = debugElement.query(By.directive(BarRatingEffect)).nativeElement; 54 | const unitElement = debugElement.query(By.css('.br-unit')).nativeElement; 55 | 56 | const event: MouseEvent = new MouseEvent('click', { bubbles: true }); 57 | unitElement.dispatchEvent(event); 58 | fixture.detectChanges(); 59 | 60 | const clone: HTMLElement = barRatingElement.querySelector('.br-unit-clone'); 61 | expect(clone).toBeTruthy(); 62 | expect(clone.style.left).toBeTruthy(); 63 | expect(clone.style.top).toBeTruthy(); 64 | expect(clone.style.width).toBeTruthy(); 65 | expect(clone.style.height).toBeTruthy(); 66 | }); 67 | 68 | it('should remove the .br-unit-clone element after the animation ends', () => { 69 | const barRatingElement: HTMLElement = debugElement.query(By.directive(BarRatingEffect)).nativeElement; 70 | const unitElement: HTMLElement = debugElement.query(By.css('.br-unit')).nativeElement; 71 | 72 | const event: MouseEvent = new MouseEvent('click', { bubbles: true }); 73 | unitElement.dispatchEvent(event); 74 | fixture.detectChanges(); 75 | 76 | const clone: HTMLElement = barRatingElement.querySelector('.br-unit-clone'); 77 | expect(clone).toBeTruthy(); 78 | 79 | clone.dispatchEvent(new Event('animationend')); 80 | fixture.detectChanges(); 81 | 82 | expect(barRatingElement.querySelector('.br-unit-clone')).toBeNull(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 8.0.1 4 | 5 | - refactor: export `BarRatinEffect` directive in `BarRatingModule`. 6 | 7 | ## 8.0.0 8 | 9 | - Upgrade to Angular 19. 10 | - Add unit tests. 11 | - feat: Add `provideBarRatingOptions` to override the default options. 12 | - feat: Add accessibility support such as focus and keys listeners to increase/decrease the rating. 13 | - feat: Ability to override CSS variables from `:root` selector. 14 | - feat: Add `effect` directive that adds click scale-fade effect. 15 | - feat: Add CSS variable `--br-effect-scale`, `--br-effect-duration` and `--br-effect-ease` to customize the effect. 16 | - fix: When used as a form control, the required validator is always true, closes [#116](https://github.com/MurhafSousli/ngx-bar-rating/issues/116). 17 | 18 | ## 7.0.1 19 | 20 | - fix: readonly attribute is not working in v7.0.0, closes [#111](https://github.com/MurhafSousli/ngx-bar-rating/issues/111). 21 | 22 | ## 7.0.0 23 | 24 | - Upgrade to Angular 18. 25 | - Upgrade to standalone components. 26 | - Remove `rxjs` dependency. 27 | - feat: Ability to use boolean inputs as attributes, such as ``. 28 | - fix: `(rateChange)` is automatically called when page is loaded, closes [#90](https://github.com/MurhafSousli/ngx-bar-rating/issues/90). 29 | - fix: `untouched`, `touched`, `dirty` classes when used as form controls. 30 | 31 | ### Breaking Changes 32 | 33 | - Rename `[readOnly]` input to `[readonly]`. 34 | - Remove `(hover)` and `(leave)` outputs. 35 | 36 | ## 6.0.0 37 | 38 | - Update to Angular 16. 39 | 40 | ## 5.0.0 41 | 42 | - Update to Angular 15, in [2f09704](https://github.com/MurhafSousli/ngx-bar-rating/pull/104/commits/2f0970415c799f28d89f1b074e86bca8c34f49c5). 43 | - Add `--br-width` and `--br-height` CSS variables to change the size of the steps for all themes except for the `default` theme, in [ba49836](https://github.com/MurhafSousli/ngx-bar-rating/pull/104/commits/ba49836d421f143b6f335f58272af2d858df62a9). 44 | - The CSS variable `--br-font-size` changes the size of the steps in `stars` and `square` themes, in [ba49836](https://github.com/MurhafSousli/ngx-bar-rating/pull/104/commits/ba49836d421f143b6f335f58272af2d858df62a9). 45 | 46 | ## 4.0.1 47 | 48 | - Update to Angular 14 in [313d97e](https://github.com/MurhafSousli/ngx-bar-rating/pull/85/commits/313d97e14db4ce957ab35f0ad31980a5f1e6ef26). 49 | 50 | ## 3.0.0 51 | 52 | - Upgrade to Angular 13, closes [#74](https://github.com/MurhafSousli/ngx-bar-rating/issues/74). 53 | - feat: Add custom bar rating directive. 54 | - feat: Add CSS variables to customize the stars. 55 | - fix: Show rating title if rating value is `null` or `0`, closes [#19](https://github.com/MurhafSousli/ngx-bar-rating/issues/19) in [d55fcc3](https://github.com/MurhafSousli/ngx-bar-rating/pull/76/commits/d55fcc3bd30b0078bf5d66853d25f398cee0f56f). 56 | - fix: Allow click event propagation, closes [#29](https://github.com/MurhafSousli/ngx-bar-rating/issues/29) in [3737458](https://github.com/MurhafSousli/ngx-bar-rating/pull/76/commits/3737458429b5979517d014c45647618e73825283). 57 | - Update demo with latest FontAwesome and Bootstrap. 58 | 59 | ### Breaking changes 60 | 61 | - Remove FontAwesome CSS themes, use custom directives instead. 62 | - Remove Bootstrap CSS themes, use custom directives instead. 63 | - When passing `[titles]` array, the first index will represent `null` or `0` value 64 | 65 | ## 2.0.0 66 | 67 | - Upgrade to Angular 10, closes [#53](https://github.com/MurhafSousli/ngx-bar-rating/issues/53). 68 | 69 | ### Breaking changes: 70 | 71 | - Remove CSS themes from the package, only SCSS themes will be shipped in the release. 72 | 73 | ## 1.0.1 74 | 75 | - remove precompiled css in src 76 | - fix custom stars class name `custom` to `stars` 77 | 78 | ## 1.0.0 79 | 80 | - feat(workflow): Compile themes sass to css for production 81 | - include themes folder in dist 82 | - improve movie rating styles, remove padding 83 | 84 | ## 0.9.0 85 | 86 | - First release 87 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-bar-rating": { 7 | "projectType": "library", 8 | "root": "projects/ngx-bar-rating", 9 | "sourceRoot": "projects/ngx-bar-rating/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/ngx-bar-rating/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/ngx-bar-rating/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/ngx-bar-rating/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:karma", 29 | "options": { 30 | "tsConfig": "projects/ngx-bar-rating/tsconfig.spec.json", 31 | "polyfills": [ 32 | "zone.js", 33 | "zone.js/testing" 34 | ], 35 | "karmaConfig": "projects/ngx-bar-rating/karma.conf.js" 36 | } 37 | }, 38 | "lint": { 39 | "builder": "@angular-eslint/builder:lint", 40 | "options": { 41 | "lintFilePatterns": [ 42 | "projects/ngx-bar-rating/**/*.ts", 43 | "projects/ngx-bar-rating/**/*.html" 44 | ] 45 | } 46 | } 47 | } 48 | }, 49 | "ngx-bar-rating-demo": { 50 | "projectType": "application", 51 | "schematics": { 52 | "@schematics/angular:component": { 53 | "style": "scss" 54 | } 55 | }, 56 | "root": "projects/ngx-bar-rating-demo", 57 | "sourceRoot": "projects/ngx-bar-rating-demo/src", 58 | "prefix": "app", 59 | "architect": { 60 | "build": { 61 | "builder": "@angular-devkit/build-angular:application", 62 | "options": { 63 | "outputPath": "dist/ngx-bar-rating-demo", 64 | "index": "projects/ngx-bar-rating-demo/src/index.html", 65 | "browser": "projects/ngx-bar-rating-demo/src/main.ts", 66 | "polyfills": [ 67 | "zone.js" 68 | ], 69 | "tsConfig": "projects/ngx-bar-rating-demo/tsconfig.app.json", 70 | "inlineStyleLanguage": "scss", 71 | "assets": [ 72 | { 73 | "glob": "**/*", 74 | "input": "projects/ngx-bar-rating-demo/public" 75 | } 76 | ], 77 | "styles": [ 78 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 79 | "node_modules/bootstrap-icons/font/bootstrap-icons.css", 80 | "projects/ngx-bar-rating-demo/src/styles.scss" 81 | ], 82 | "scripts": [], 83 | "server": "projects/ngx-bar-rating-demo/src/main.server.ts", 84 | "prerender": true, 85 | "ssr": { 86 | "entry": "projects/ngx-bar-rating-demo/src/server.ts" 87 | } 88 | }, 89 | "configurations": { 90 | "production": { 91 | "budgets": [ 92 | { 93 | "type": "initial", 94 | "maximumWarning": "2mb", 95 | "maximumError": "3mb" 96 | }, 97 | { 98 | "type": "anyComponentStyle", 99 | "maximumWarning": "15kb", 100 | "maximumError": "15kb" 101 | } 102 | ], 103 | "outputHashing": "all" 104 | }, 105 | "development": { 106 | "optimization": false, 107 | "extractLicenses": false, 108 | "sourceMap": true 109 | } 110 | }, 111 | "defaultConfiguration": "production" 112 | }, 113 | "serve": { 114 | "builder": "@angular-devkit/build-angular:dev-server", 115 | "configurations": { 116 | "production": { 117 | "buildTarget": "ngx-bar-rating-demo:build:production" 118 | }, 119 | "development": { 120 | "buildTarget": "ngx-bar-rating-demo:build:development" 121 | } 122 | }, 123 | "defaultConfiguration": "development" 124 | }, 125 | "extract-i18n": { 126 | "builder": "@angular-devkit/build-angular:extract-i18n", 127 | "options": { 128 | "buildTarget": "ngx-bar-rating-demo:build" 129 | } 130 | }, 131 | "test": { 132 | "builder": "@angular-devkit/build-angular:karma", 133 | "options": { 134 | "polyfills": [ 135 | "zone.js", 136 | "zone.js/testing" 137 | ], 138 | "tsConfig": "projects/ngx-bar-rating-demo/tsconfig.spec.json", 139 | "inlineStyleLanguage": "scss", 140 | "assets": [ 141 | "projects/ngx-bar-rating-demo/src/favicon.ico", 142 | "projects/ngx-bar-rating-demo/src/assets" 143 | ], 144 | "styles": [ 145 | "projects/ngx-bar-rating-demo/src/styles.scss" 146 | ], 147 | "scripts": [] 148 | } 149 | } 150 | } 151 | } 152 | }, 153 | "cli": { 154 | "schematicCollections": [ 155 | "@angular-eslint/schematics" 156 | ] 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | inject, 4 | signal, 5 | output, 6 | computed, 7 | forwardRef, 8 | numberAttribute, 9 | booleanAttribute, 10 | input, 11 | model, 12 | contentChild, 13 | Signal, 14 | Provider, 15 | InputSignal, 16 | ModelSignal, 17 | WritableSignal, 18 | OutputEmitterRef, 19 | ChangeDetectionStrategy, 20 | InputSignalWithTransform 21 | } from '@angular/core'; 22 | import { NgTemplateOutlet } from '@angular/common'; 23 | import { ControlValueAccessor, Validator, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; 24 | import { ActiveRating, FractionRating, InactiveRating } from './custom-rating'; 25 | import { BAR_RATING_OPTIONS, BarRatingOptions } from './bar-rating.model'; 26 | 27 | /** This allows support [(ngModel)] and ngControl. */ 28 | const RATING_VALUE_ACCESSOR: Provider = { 29 | provide: NG_VALUE_ACCESSOR, 30 | useExisting: forwardRef(() => BarRating), 31 | multi: true 32 | }; 33 | 34 | /** This allows control required validation. */ 35 | const RATING_VALUE_VALIDATOR: Provider = { 36 | provide: NG_VALIDATORS, 37 | useExisting: forwardRef(() => BarRating), 38 | multi: true, 39 | }; 40 | 41 | enum BarRatingUnitStateEnum { 42 | active = 'active', 43 | inactive = 'inactive', 44 | selected = 'selected', 45 | fraction = 'fraction' 46 | } 47 | 48 | type BarRatingUnitState = `${ BarRatingUnitStateEnum }`; 49 | 50 | @Component({ 51 | selector: 'bar-rating', 52 | templateUrl: './bar-rating.html', 53 | styleUrl: './bar-rating.scss', 54 | providers: [RATING_VALUE_ACCESSOR, RATING_VALUE_VALIDATOR], 55 | imports: [NgTemplateOutlet], 56 | changeDetection: ChangeDetectionStrategy.OnPush 57 | }) 58 | export class BarRating implements ControlValueAccessor, Validator { 59 | 60 | readonly defaultOptions: BarRatingOptions = inject(BAR_RATING_OPTIONS); 61 | 62 | onChange: OnChangeFn = () => { 63 | }; 64 | 65 | onTouched: OnTouchedFn = () => { 66 | }; 67 | 68 | readonly UNITS: typeof BarRatingUnitStateEnum = BarRatingUnitStateEnum; 69 | 70 | disabled: boolean; 71 | 72 | /** Current rating. Can be a decimal value like 3.14 */ 73 | rate: ModelSignal = model(); 74 | 75 | /** Maximal rating that can be given using this widget. */ 76 | max: InputSignalWithTransform = input(this.defaultOptions.maxValue, { 77 | transform: numberAttribute 78 | }); 79 | 80 | /** A flag indicating if rating can be updated. */ 81 | readonly: InputSignalWithTransform = input(this.defaultOptions.readonly, { 82 | transform: booleanAttribute, 83 | alias: 'readonly' 84 | }); 85 | 86 | /** Set the theme */ 87 | theme: InputSignal = input(this.defaultOptions.theme); 88 | 89 | /** Show rating title */ 90 | showText: InputSignalWithTransform = input(this.defaultOptions.showText, { 91 | transform: booleanAttribute 92 | }); 93 | 94 | /** Replace rate value with a title */ 95 | titles: InputSignal = input([]); 96 | 97 | /** A flag indicating if rating is required for form validation. */ 98 | required: InputSignalWithTransform = input(false, { transform: booleanAttribute }); 99 | 100 | tabIndex: InputSignalWithTransform = input(0, { transform: numberAttribute }); 101 | 102 | hoveredIndex: WritableSignal = signal(null); 103 | 104 | contexts: Signal = computed(() => { 105 | const length: number = this.max(); 106 | const currentRate: number = this.rate(); 107 | const hovered: number = this.hoveredIndex(); 108 | 109 | return Array.from({ length }, (_: unknown, i: number) => { 110 | if (hovered) { 111 | return i + 1 <= hovered ? BarRatingUnitStateEnum.active : BarRatingUnitStateEnum.inactive; 112 | } 113 | if (i + 1 <= currentRate) { 114 | return BarRatingUnitStateEnum.selected; 115 | } 116 | if ((i + 1 === Math.round(currentRate) && currentRate % 1) >= 0.5) { 117 | return BarRatingUnitStateEnum.fraction; 118 | } 119 | return BarRatingUnitStateEnum.inactive; 120 | }); 121 | }); 122 | 123 | ratingText: Signal = computed(() => { 124 | const value: string | number = this.hoveredIndex() || this.rate(); 125 | return this.titles()[value] || value; 126 | }); 127 | 128 | /** 129 | * A stream that forwards a bar rating click since clicks are not propagated 130 | */ 131 | barClick: OutputEmitterRef = output(); 132 | 133 | customActiveRating: Signal = contentChild(ActiveRating); 134 | customInActiveRating: Signal = contentChild(InactiveRating); 135 | customFractionRating: Signal = contentChild(FractionRating); 136 | 137 | updateRating(value: number): void { 138 | this.rate.set(value); 139 | this.onChange(value); 140 | } 141 | 142 | /** 143 | * This is the initial value set to the component 144 | */ 145 | writeValue(value: number): void { 146 | if (value !== null) { 147 | this.rate.set(value); 148 | } 149 | } 150 | 151 | validate(c: UntypedFormControl): { required: boolean } | null { 152 | return (this.required() && !c.value) ? { required: true } : null; 153 | } 154 | 155 | registerOnChange(fn: OnChangeFn): void { 156 | this.onChange = fn; 157 | } 158 | 159 | registerOnTouched(fn: () => OnTouchedFn): void { 160 | this.onTouched = fn; 161 | } 162 | 163 | setDisabledState(isDisabled: boolean): void { 164 | this.disabled = isDisabled; 165 | } 166 | 167 | handleKeydown(event: KeyboardEvent): void { 168 | if (this.readonly() || this.disabled) return; 169 | 170 | const currentRating: number = this.rate(); 171 | let newRating: number = currentRating; 172 | 173 | switch (event.key) { 174 | case 'ArrowRight': 175 | newRating = Math.min(currentRating + 1, this.max()); // Increase rating 176 | event.preventDefault(); 177 | break; 178 | case 'ArrowLeft': 179 | newRating = Math.max(currentRating - 1, 1); // Decrease rating 180 | event.preventDefault(); 181 | break; 182 | case 'Enter': 183 | case ' ': 184 | // Select the current rating (handled in click logic) 185 | event.preventDefault(); 186 | break; 187 | default: 188 | return; // Ignore other keys 189 | } 190 | 191 | if (newRating !== currentRating) { 192 | this.updateRating(newRating); 193 | this.barClick.emit(newRating); 194 | this.hoveredIndex.set(newRating); 195 | } 196 | } 197 | 198 | handleClick(event: MouseEvent): void { 199 | if (this.readonly() || this.disabled) return; 200 | 201 | const target = event.target as HTMLElement; 202 | const unitElement = target.closest('.br-unit') as HTMLElement; 203 | const value: string = unitElement?.getAttribute('data-value'); 204 | 205 | if (value) { 206 | const rating: number = parseInt(value, 10); 207 | this.updateRating(rating); 208 | this.barClick.emit(rating); 209 | } 210 | } 211 | } 212 | 213 | type OnChangeFn = (value: T) => void; 214 | type OnTouchedFn = () => void; 215 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/003-bellsprout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 53 | 54 | 55 | 57 | 58 | 59 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 71 | 72 | 73 | 74 | 75 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 93 | 94 | 95 | 97 | 98 | 99 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 111 | 112 | 113 | 115 | 116 | 117 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/004-squirtle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 44 | 45 | 46 | 48 | 49 | 50 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 83 | 84 | 85 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 105 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/007-pikachu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 39 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 77 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 108 | 110 | 111 | 112 | 113 | 114 | 119 | 120 | 121 | 123 | 124 | 125 | 126 | 127 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Angular Bar Rating

4 |

Minimal, light-weight Angular ratings.

5 |

6 | 7 | [![npm](https://img.shields.io/badge/demo-online-ed1c46.svg)](https://ngx-bar-rating.netlify.app/) 8 | [![npm](https://img.shields.io/badge/stackblitz-online-orange.svg)](https://stackblitz.com/edit/ngx-bar-rating) 9 | [![npm](https://img.shields.io/npm/v/ngx-bar-rating.svg)](https://www.npmjs.com/package/ngx-bar-rating) 10 | [![CI Build](https://github.com/MurhafSousli/ngx-bar-rating/actions/workflows/integrate.yml/badge.svg)](https://github.com/MurhafSousli/ngx-bar-rating/actions/workflows/integrate.yml)[![codecov](https://codecov.io/gh/MurhafSousli/ngx-bar-rating/graph/badge.svg?token=XH3LVM5QS6)](https://codecov.io/gh/MurhafSousli/ngx-bar-rating) 11 | [![Downloads](https://img.shields.io/npm/dt/ngx-bar-rating.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/ngx-bar-rating) 12 | [![Monthly Downloads](https://img.shields.io/npm/dm/ngx-bar-rating.svg)](https://www.npmjs.com/package/ngx-bar-rating) 13 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/ngx-bar-rating.svg)](https://bundlephobia.com/result?p=ngx-bar-rating) 14 | [![npm](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](/LICENSE) 15 | 16 | ___ 17 | 18 | If you like this plugin, please give it a star ⭐. 19 | 20 | ## Table of Contents 21 | 22 | - [Live Demo](https://ngx-bar-rating.netlify.app/) 23 | - [Installation](#installation) 24 | - [Usage](#usage) 25 | - [Options](#options) 26 | - [Click Effect](#click-effect) 27 | - [CSS Variables](#css-variables) 28 | - [Themes](#themes) 29 | - [Issues](#issues) 30 | - [Author](#author) 31 | 32 | 33 | 34 | ## Installation 35 | 36 | Install it with npm 37 | 38 | ```bash 39 | npm i ngx-bar-rating 40 | ``` 41 | 42 | 43 | 44 | 45 | ## Basic usage: 46 | 47 | Import `BarRating` or `BarRatingModule` in your component imports. 48 | 49 | ```ts 50 | @Component({ 51 | standalone: true, 52 | selector: 'bars', 53 | template: ` 54 | 55 | `, 56 | styleUrl: './example.component.scss', 57 | imports: [BarRating] 58 | }) 59 | export class BarsComponent { 60 | rate: number = 4; 61 | } 62 | ``` 63 | 64 | Import the theme in your global styles (unless you want to use custom template) 65 | 66 | ```scss 67 | @import 'ngx-bar-rating/themes/br-default-theme'; 68 | ``` 69 | 70 | 71 | 72 | ## Rating inputs and outputs: 73 | 74 | | Name | Description | Default | 75 | |------------------|----------------------------------------------------------------------|---------| 76 | | **[rate]** | Current rating. Can be a decimal value like 3.14 | null | 77 | | **[max]** | Maximal rating that can be given using this widget | 5 | 78 | | **[theme]** | Theme class, see available [themes](#themes) | default | 79 | | **[readonly]** | A flag that indicates if rating can be changed | false | 80 | | **[showText]** | Display rating title if available, otherwise display rating value | false | 81 | | **[required]** | A flag that indicates if rating is disabled. works only with forms | false | 82 | | **[disabled]** | A flag that indicates if rating is disabled. works only with forms | false | 83 | | **[titles]** | Titles array. array should represent all possible values including 0 | [] | 84 | | **(rateChange)** | A stream that emits when the rating value is changed | | 85 | 86 | ### Custom rating template 87 | 88 | `BarRatingModule` provides a couple of directives to set custom rating template of your choice. 89 | 90 | - `*ratingActive`: Set template, when a bar/star is active or hovered. 91 | - `*ratingInactive`: Set template, when a bar/star is inactive. 92 | - `*ratingFraction`: Set template, when a bar/star is a fraction. 93 | 94 | Here are some example: 95 | 96 | #### Bootstrap rating example 97 | 98 | ```html 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | #### FontAwesome rating example 106 | 107 | ```html 108 | 109 | 110 | 111 | 112 | 113 | ``` 114 | 115 | #### Movie rating example 116 | 117 | ```html 118 | 120 | ``` 121 | 122 | It can be used with Angular forms: 123 | 124 | ```html 125 |
126 | 127 | 128 |

form is valid: {{ form.valid ? 'true' : 'false' }}

129 |
{{ formRating }}
130 | ``` 131 | 132 | And reactive forms: 133 | 134 | ```html 135 |
136 | 137 | 138 |

form is valid: {{ form.valid ? 'true' : 'false' }}

139 |
{{ formRating }}
140 | ``` 141 | 142 |
143 | 144 | ## Click effect 145 | 146 | To apply a scale-fade effect when a bar is clicked, simply add the `effect` directive to the `` component: 147 | 148 | ```html 149 | 150 | ``` 151 | 152 | ## CSS variables 153 | 154 | * `--br-font-size`: Defines the font size for the step element. Affects the themes: [`default`, `square`, `stars`]. 155 | * `--br-width`: Specifies the width of the step element. Affects the themes: [`stars`, `square`, `movie`, `vertical`, `horizontal`]. 156 | * `--br-height`: Specifies the height of the step element. Affects the themes: [`stars`, `square`, `movie`, `vertical`, `horizontal`]. 157 | * `--br-gap`: Sets the gap between the individual steps. 158 | * `--br-active-color`: Defines the color for active steps. 159 | * `--br-inactive-color`: Defines the color for inactive steps. 160 | * `--br-effect-scale`: Specifies the scale value for the scale-fade effect (e.g., `2`). 161 | * `--br-effect-duration`: Sets the duration of the scale-fade animation (e.g., `0.4s`). 162 | * `--br-effect-ease`: Defines the easing function for the scale-fade animation (e.g., `ease-out`). 163 | 164 | 165 | 166 | ## Predefined themes 167 | 168 | > If you want to use a custom rating template, you don't need to import any CSS theme. 169 | 170 | If you want to use one of the predefined themes, you will need to import it in the global style `style.scss` 171 | 172 | - Pure CSS stars (default) `theme="default"` 173 | 174 | ```css 175 | @import 'ngx-bar-rating/themes/br-default-theme'; 176 | ``` 177 | 178 | - Horizontal bars `theme="horizontal"` 179 | 180 | ```css 181 | @import 'ngx-bar-rating/themes/br-horizontal-theme'; 182 | ``` 183 | 184 | - Vertical bars `theme="vertical"` 185 | 186 | ```css 187 | @import 'ngx-bar-rating/themes/br-vertical-theme'; 188 | ``` 189 | 190 | - Custom stars `theme="stars"` 191 | 192 | ```css 193 | @import 'ngx-bar-rating/themes/br-stars-theme'; 194 | ``` 195 | 196 | - Movie rating `theme="movie"` 197 | 198 | ```css 199 | @import 'ngx-bar-rating/themes/br-movie-theme'; 200 | ``` 201 | 202 | - Square rating `theme="square"` 203 | 204 | ```css 205 | @import 'ngx-bar-rating/themes/br-square-theme'; 206 | ``` 207 | 208 | Rating style can be easily customized, check the classes used in any theme and add your own css. 209 | 210 | You can also do the same for forms classes such as `untouched, touched, dirty, invalid, valid ...etc` 211 | 212 | 213 | 214 | ## Issues 215 | 216 | If you identify any errors in this component, or have an idea for an improvement, please open 217 | an [issue](https://github.com/MurhafSousli/ngx-bar-rating/issues). I am excited to see what the community thinks of this 218 | project, and I would love your input! 219 | 220 | 221 | 222 | ## Author 223 | 224 | **Murhaf Sousli** 225 | 226 | - [github/murhafsousli](https://github.com/MurhafSousli) 227 | - [twitter/murhafsousli](https://twitter.com/MurhafSousli) 228 | 229 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Angular Bar Rating

4 |

Minimal, light-weight Angular ratings.

5 |

6 | 7 | [![npm](https://img.shields.io/badge/demo-online-ed1c46.svg)](https://ngx-bar-rating.netlify.app/) 8 | [![npm](https://img.shields.io/badge/stackblitz-online-orange.svg)](https://stackblitz.com/edit/ngx-bar-rating) 9 | [![npm](https://img.shields.io/npm/v/ngx-bar-rating.svg)](https://www.npmjs.com/package/ngx-bar-rating) 10 | [![CI Build](https://github.com/MurhafSousli/ngx-bar-rating/actions/workflows/integrate.yml/badge.svg)](https://github.com/MurhafSousli/ngx-bar-rating/actions/workflows/integrate.yml)[![codecov](https://codecov.io/gh/MurhafSousli/ngx-bar-rating/graph/badge.svg?token=XH3LVM5QS6)](https://codecov.io/gh/MurhafSousli/ngx-bar-rating) 11 | [![Downloads](https://img.shields.io/npm/dt/ngx-bar-rating.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/ngx-bar-rating) 12 | [![Monthly Downloads](https://img.shields.io/npm/dm/ngx-bar-rating.svg)](https://www.npmjs.com/package/ngx-bar-rating) 13 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/ngx-bar-rating.svg)](https://bundlephobia.com/result?p=ngx-bar-rating) 14 | [![npm](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](/LICENSE) 15 | 16 | ___ 17 | 18 | If you like this plugin, please give it a star ⭐. 19 | 20 | ## Table of Contents 21 | 22 | - [Live Demo](https://ngx-bar-rating.netlify.app/) 23 | - [Installation](#installation) 24 | - [Usage](#usage) 25 | - [Options](#options) 26 | - [Click Effect](#click-effect) 27 | - [CSS Variables](#css-variables) 28 | - [Themes](#themes) 29 | - [Issues](#issues) 30 | - [Author](#author) 31 | 32 |
33 | 34 | ## Installation 35 | 36 | Install it with npm 37 | 38 | ```bash 39 | npm i ngx-bar-rating 40 | ``` 41 | 42 | 43 | 44 | 45 | ## Basic usage: 46 | 47 | Import `BarRating` or `BarRatingModule` in your component imports. 48 | 49 | ```ts 50 | @Component({ 51 | standalone: true, 52 | selector: 'bars', 53 | template: ` 54 | 55 | `, 56 | styleUrl: './example.component.scss', 57 | imports: [BarRating] 58 | }) 59 | export class BarsComponent { 60 | rate: number = 4; 61 | } 62 | ``` 63 | 64 | Import the theme in your global styles (unless you want to use custom template) 65 | 66 | ```scss 67 | @import 'ngx-bar-rating/themes/br-default-theme'; 68 | ``` 69 | 70 | 71 | 72 | ## Rating inputs and outputs: 73 | 74 | | Name | Description | Default | 75 | |------------------|----------------------------------------------------------------------|---------| 76 | | **[rate]** | Current rating. Can be a decimal value like 3.14 | null | 77 | | **[max]** | Maximal rating that can be given using this widget | 5 | 78 | | **[theme]** | Theme class, see available [themes](#themes) | default | 79 | | **[readonly]** | A flag that indicates if rating can be changed | false | 80 | | **[showText]** | Display rating title if available, otherwise display rating value | false | 81 | | **[required]** | A flag that indicates if rating is disabled. works only with forms | false | 82 | | **[disabled]** | A flag that indicates if rating is disabled. works only with forms | false | 83 | | **[titles]** | Titles array. array should represent all possible values including 0 | [] | 84 | | **(rateChange)** | A stream that emits when the rating value is changed | | 85 | 86 | ### Custom rating template 87 | 88 | `BarRatingModule` provides a couple of directives to set custom rating template of your choice. 89 | 90 | - `*ratingActive`: Set template, when a bar/star is active or hovered. 91 | - `*ratingInactive`: Set template, when a bar/star is inactive. 92 | - `*ratingFraction`: Set template, when a bar/star is a fraction. 93 | 94 | Here are some example: 95 | 96 | #### Bootstrap rating example 97 | 98 | ```html 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | #### FontAwesome rating example 106 | 107 | ```html 108 | 109 | 110 | 111 | 112 | 113 | ``` 114 | 115 | #### Movie rating example 116 | 117 | ```html 118 | 120 | ``` 121 | 122 | It can be used with Angular forms: 123 | 124 | ```html 125 |
126 | 127 | 128 |

form is valid: {{ form.valid ? 'true' : 'false' }}

129 |
{{ formRating }}
130 | ``` 131 | 132 | And reactive forms: 133 | 134 | ```html 135 |
136 | 137 | 138 |

form is valid: {{ form.valid ? 'true' : 'false' }}

139 |
{{ formRating }}
140 | ``` 141 | 142 |
143 | 144 | ## Click effect 145 | 146 | To apply a scale-fade effect when a bar is clicked, simply add the `effect` directive to the `` component: 147 | 148 | ```html 149 | 150 | ``` 151 | 152 | ## CSS variables 153 | 154 | * `--br-font-size`: Defines the font size for the step element. Affects the themes: [`default`, `square`, `stars`]. 155 | * `--br-width`: Specifies the width of the step element. Affects the themes: [`stars`, `square`, `movie`, `vertical`, `horizontal`]. 156 | * `--br-height`: Specifies the height of the step element. Affects the themes: [`stars`, `square`, `movie`, `vertical`, `horizontal`]. 157 | * `--br-gap`: Sets the gap between the individual steps. 158 | * `--br-active-color`: Defines the color for active steps. 159 | * `--br-inactive-color`: Defines the color for inactive steps. 160 | * `--br-effect-scale`: Specifies the scale value for the scale-fade effect (e.g., `2`). 161 | * `--br-effect-duration`: Sets the duration of the scale-fade animation (e.g., `0.4s`). 162 | * `--br-effect-ease`: Defines the easing function for the scale-fade animation (e.g., `ease-out`). 163 | 164 | 165 | 166 | ## Predefined themes 167 | 168 | > If you want to use a custom rating template, you don't need to import any CSS theme. 169 | 170 | If you want to use one of the predefined themes, you will need to import it in the global style `style.scss` 171 | 172 | - Pure CSS stars (default) `theme="default"` 173 | 174 | ```css 175 | @import 'ngx-bar-rating/themes/br-default-theme'; 176 | ``` 177 | 178 | - Horizontal bars `theme="horizontal"` 179 | 180 | ```css 181 | @import 'ngx-bar-rating/themes/br-horizontal-theme'; 182 | ``` 183 | 184 | - Vertical bars `theme="vertical"` 185 | 186 | ```css 187 | @import 'ngx-bar-rating/themes/br-vertical-theme'; 188 | ``` 189 | 190 | - Custom stars `theme="stars"` 191 | 192 | ```css 193 | @import 'ngx-bar-rating/themes/br-stars-theme'; 194 | ``` 195 | 196 | - Movie rating `theme="movie"` 197 | 198 | ```css 199 | @import 'ngx-bar-rating/themes/br-movie-theme'; 200 | ``` 201 | 202 | - Square rating `theme="square"` 203 | 204 | ```css 205 | @import 'ngx-bar-rating/themes/br-square-theme'; 206 | ``` 207 | 208 | Rating style can be easily customized, check the classes used in any theme and add your own css. 209 | 210 | You can also do the same for forms classes such as `untouched, touched, dirty, invalid, valid ...etc` 211 | 212 | 213 | 214 | ## Issues 215 | 216 | If you identify any errors in this component, or have an idea for an improvement, please open 217 | an [issue](https://github.com/MurhafSousli/ngx-bar-rating/issues). I am excited to see what the community thinks of this 218 | project, and I would love your input! 219 | 220 | 221 | 222 | ## Author 223 | 224 | **Murhaf Sousli** 225 | 226 | - [github/murhafsousli](https://github.com/MurhafSousli) 227 | - [twitter/murhafsousli](https://twitter.com/MurhafSousli) 228 | 229 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/001-caterpie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 15 | 17 | 18 | 19 | 21 | 22 | 23 | 25 | 26 | 27 | 29 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 47 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 73 | 74 | 75 | 78 | 79 | 90 | 91 | 92 | 94 | 95 | 96 | 98 | 99 | 100 | 102 | 103 | 104 | 106 | 107 | 108 | 112 | 113 | 114 | 115 | 116 | 118 | 119 | 120 | 122 | 123 | 124 | 126 | 127 | 128 | 130 | 131 | 132 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating/src/lib/bar-rating.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { 4 | FormsModule, 5 | ReactiveFormsModule, 6 | ControlValueAccessor, 7 | NG_VALIDATORS, 8 | NG_VALUE_ACCESSOR, 9 | UntypedFormControl 10 | } from '@angular/forms'; 11 | import { BarRating } from 'ngx-bar-rating'; 12 | 13 | describe('BarRating Component', () => { 14 | let component: BarRating; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(async () => { 18 | await TestBed.configureTestingModule({ 19 | imports: [FormsModule, ReactiveFormsModule, BarRating], 20 | }).compileComponents(); 21 | 22 | fixture = TestBed.createComponent(BarRating); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create the component', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | 31 | it('should register BarRating as a provider for NG_VALUE_ACCESSOR', () => { 32 | const injector = fixture.debugElement.injector; 33 | 34 | const valueAccessorProviders = injector.get(NG_VALUE_ACCESSOR); 35 | expect(valueAccessorProviders.some(provider => provider instanceof BarRating)).toBeTrue(); 36 | }); 37 | 38 | it('should register BarRating as a provider for NG_VALIDATORS', () => { 39 | const injector = fixture.debugElement.injector; 40 | 41 | const valueAccessorProviders = injector.get(NG_VALIDATORS); 42 | expect(valueAccessorProviders.some(provider => provider instanceof BarRating)).toBeTrue(); 43 | }); 44 | 45 | it('should call writeValue, registerOnChange, and registerOnTouched', () => { 46 | const onChangeSpy = jasmine.createSpy('onChange'); 47 | const onTouchedSpy = jasmine.createSpy('onTouched'); 48 | 49 | // Register onChange and onTouched 50 | component.registerOnChange(onChangeSpy); 51 | component.registerOnTouched(onTouchedSpy); 52 | 53 | // Call writeValue and verify value propagation 54 | component.writeValue(3); 55 | expect(component.rate()).toBe(3); 56 | 57 | // Simulate interaction 58 | component.updateRating(4); 59 | expect(onChangeSpy).toHaveBeenCalledWith(4); 60 | 61 | // Simulate touch 62 | component.onTouched(); 63 | expect(onTouchedSpy).toHaveBeenCalled(); 64 | }); 65 | 66 | it('should initialize with default options', () => { 67 | expect(component.rate()).toBeUndefined(); 68 | expect(component.max()).toBe(component.defaultOptions.maxValue); 69 | expect(component.readonly()).toBe(component.defaultOptions.readonly); 70 | }); 71 | 72 | it('should update rating and emit onChange', () => { 73 | const spy = spyOn(component, 'onChange'); 74 | component.updateRating(4); 75 | expect(component.rate()).toBe(4); 76 | expect(spy).toHaveBeenCalledWith(4); 77 | }); 78 | 79 | it('should validate required input', () => { 80 | fixture.componentRef.setInput('required', true); 81 | const control = new UntypedFormControl(''); 82 | const validationResult = component.validate(control); 83 | expect(validationResult).toEqual({ required: true }); 84 | 85 | control.setValue(3); 86 | expect(component.validate(control)).toBeNull(); 87 | }); 88 | 89 | it('should handle writeValue correctly', () => { 90 | component.writeValue(5); 91 | expect(component.rate()).toBe(5); 92 | }); 93 | 94 | it('should set disabled state', () => { 95 | component.setDisabledState(true); 96 | expect(component.disabled).toBeTrue(); 97 | }); 98 | 99 | it('should compute correct contexts for units', () => { 100 | fixture.componentRef.setInput('max', 5); 101 | component.updateRating(3.5); 102 | fixture.detectChanges(); 103 | 104 | const contexts = component.contexts(); 105 | expect(contexts).toEqual([ 106 | 'selected', 107 | 'selected', 108 | 'selected', 109 | 'fraction', 110 | 'inactive' 111 | ]); 112 | }); 113 | 114 | it('should compute correct rating text', () => { 115 | fixture.componentRef.setInput('titles', ['Poor', 'Fair', 'Good', 'Very Good', 'Excellent']); 116 | component.updateRating(3); 117 | fixture.detectChanges(); 118 | 119 | expect(component.ratingText()).toBe('Very Good'); 120 | }); 121 | 122 | it('should display rating value if titles input is not provided', () => { 123 | component.updateRating(2); 124 | fixture.detectChanges(); 125 | 126 | expect(component.ratingText()).toBe(2); 127 | }); 128 | 129 | it('should compute correct contexts for units when hovered', () => { 130 | component.hoveredIndex.set(3); 131 | fixture.detectChanges(); 132 | 133 | expect(component.contexts()).toEqual([ 134 | 'active', 135 | 'active', 136 | 'active', 137 | 'inactive', 138 | 'inactive' 139 | ]); 140 | }); 141 | 142 | describe('handleKeydown', () => { 143 | it('should increase rating with ArrowRight', () => { 144 | component.rate.set(3); 145 | const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); 146 | component.handleKeydown(event); 147 | expect(component.rate()).toBe(4); // Increased from 3 to 4 148 | }); 149 | 150 | it('should decrease rating with ArrowLeft', () => { 151 | component.rate.set(3); 152 | const event = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); 153 | component.handleKeydown(event); 154 | expect(component.rate()).toBe(2); // Decreased from 3 to 2 155 | }); 156 | 157 | it('should not increase rating above max', () => { 158 | component.rate.set(5); // Set to max 159 | const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); 160 | component.handleKeydown(event); 161 | expect(component.rate()).toBe(5); // Remains at max 162 | }); 163 | 164 | it('should not decrease rating below 1', () => { 165 | component.rate.set(1); // Set to min 166 | const event = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); 167 | component.handleKeydown(event); 168 | expect(component.rate()).toBe(1); // Remains at min 169 | }); 170 | 171 | it('should emit barClick event when rating changes', () => { 172 | component.rate.set(3); 173 | spyOn(component.barClick, 'emit'); 174 | const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); 175 | component.handleKeydown(event); 176 | expect(component.barClick.emit).toHaveBeenCalledWith(4); // Emit new rating 177 | }); 178 | 179 | it('should do nothing for unsupported keys', () => { 180 | component.rate.set(3); 181 | const event = new KeyboardEvent('keydown', { key: 'a' }); 182 | component.handleKeydown(event); 183 | expect(component.rate()).toBe(3); // Unchanged 184 | }); 185 | 186 | it('should not process keydown if readonly or disabled', () => { 187 | component.rate.set(3); 188 | fixture.componentRef.setInput('readonly', true); 189 | const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); 190 | component.handleKeydown(event); 191 | expect(component.rate()).toBe(3); // Unchanged 192 | 193 | fixture.componentRef.setInput('readonly', false); 194 | component.disabled = true; 195 | component.handleKeydown(event); 196 | expect(component.rate()).toBe(3); // Unchanged 197 | }); 198 | 199 | it('should handle Enter key without modifying the rate', () => { 200 | spyOn(component, 'updateRating'); // Spy on updateRating method 201 | spyOn(component.barClick, 'emit'); // Spy on barClick emitter 202 | 203 | // Set initial rate 204 | component.rate.set(3); 205 | 206 | // Simulate keydown for Enter key 207 | const event = new KeyboardEvent('keydown', { key: 'Enter' }); 208 | component.handleKeydown(event); 209 | 210 | // Assert that updateRating was not called 211 | expect(component.updateRating).not.toHaveBeenCalled(); 212 | 213 | // Assert that barClick was not emitted 214 | expect(component.barClick.emit).not.toHaveBeenCalled(); 215 | 216 | // Assert that rate remains unchanged 217 | expect(component.rate()).toBe(3); 218 | }); 219 | 220 | it('should handle Space key without modifying the rate', () => { 221 | spyOn(component, 'updateRating'); // Spy on updateRating method 222 | spyOn(component.barClick, 'emit'); // Spy on barClick emitter 223 | 224 | // Set initial rate 225 | component.rate.set(3); 226 | 227 | // Simulate keydown for Space key 228 | const event = new KeyboardEvent('keydown', { key: ' ' }); 229 | component.handleKeydown(event); 230 | 231 | // Assert that updateRating was not called 232 | expect(component.updateRating).not.toHaveBeenCalled(); 233 | 234 | // Assert that barClick was not emitted 235 | expect(component.barClick.emit).not.toHaveBeenCalled(); 236 | 237 | // Assert that rate remains unchanged 238 | expect(component.rate()).toBe(3); 239 | }); 240 | }); 241 | 242 | describe('handleClick', () => { 243 | it('should update rating when clicking on a bar', () => { 244 | const target = fixture.debugElement.query(By.css('.br-unit[data-value="4"]')); 245 | const event = new MouseEvent('click', { bubbles: true }); 246 | spyOn(component.barClick, 'emit'); 247 | 248 | target.nativeElement.dispatchEvent(event); 249 | component.handleClick(event); 250 | 251 | expect(component.rate()).toBe(4); // Updated to clicked value 252 | expect(component.barClick.emit).toHaveBeenCalledWith(4); // Emit clicked rating 253 | }); 254 | 255 | it('should not process click if readonly or disabled', () => { 256 | component.rate.set(3); 257 | fixture.componentRef.setInput('readonly', true); 258 | 259 | const target = fixture.debugElement.query(By.css('.br-unit[data-value="4"]')); 260 | const event = new MouseEvent('click', { bubbles: true }); 261 | 262 | target.nativeElement.dispatchEvent(event); 263 | component.handleClick(event); 264 | 265 | expect(component.rate()).toBe(3); // Unchanged 266 | 267 | fixture.componentRef.setInput('readonly', false); 268 | component.disabled = true; 269 | 270 | component.handleClick(event); 271 | expect(component.rate()).toBe(3); // Unchanged 272 | }); 273 | 274 | it('should do nothing if clicking outside a bar', () => { 275 | component.rate.set(3); 276 | const event = new MouseEvent('click', { bubbles: true }); 277 | const outsideTarget = fixture.debugElement.query(By.css('.br')).nativeElement; 278 | 279 | outsideTarget.dispatchEvent(event); 280 | component.handleClick(event); 281 | 282 | expect(component.rate()).toBe(3); // Unchanged 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/006-bullbasaur.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 15 | 16 | 17 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 57 | 58 | 59 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 97 | 99 | 100 | 101 | 104 | 105 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 117 | 118 | 119 | 122 | 123 | 124 | 129 | 130 | 131 | 132 | 133 | 136 | 137 | 138 | 141 | 142 | 143 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/002-venonat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 96 | 98 | 99 | 100 | 102 | 103 | 104 | 111 | 112 | 113 | 114 | 115 | 117 | 118 | 119 | 121 | 122 | 123 | 129 | 130 | 131 | 132 | 133 | 135 | 136 | 137 | 138 | 139 | 143 | 145 | 146 | 148 | 149 | 150 | 151 | 152 | 156 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 185 | 186 | 187 | 188 | 189 | 193 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/008-charmander.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | 83 | 84 | 85 | 87 | 88 | 89 | 91 | 92 | 93 | 97 | 98 | 99 | 100 | 102 | 103 | 104 | 105 | 106 | 110 | 111 | 112 | 114 | 115 | 116 | 117 | 118 | 123 | 125 | 126 | 127 | 130 | 131 | 132 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 154 | 155 | 156 | 158 | 159 | 160 | 161 | 162 | 163 | 165 | 166 | 167 | 168 | 169 | 170 | 175 | 176 | 177 | 178 | 179 | 181 | 182 | 183 | 184 | 185 | 186 | 188 | 189 | 190 | 191 | 192 | 193 | 198 | 199 | 200 | 201 | 202 | 204 | 205 | 206 | 207 | 208 | 209 | 211 | 212 | 213 | 214 | 215 | 216 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /projects/ngx-bar-rating-demo/public/custom/005-jigglypuff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 77 | 78 | 79 | 81 | 82 | 104 | 105 | 107 | 108 | 109 | 111 | 112 | 113 | 115 | 116 | 133 | 134 | 135 | 137 | 138 | 139 | 141 | 142 | 143 | 145 | 146 | 147 | 151 | 152 | 153 | 154 | 156 | 157 | 158 | 159 | 160 | 165 | 166 | 167 | 169 | 170 | 171 | 172 | 173 | 178 | 179 | 180 | 182 | 183 | 184 | 185 | 186 | 187 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | --------------------------------------------------------------------------------