├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── karma.conf.js ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── ghcorner │ │ ├── ghcorner.component.css │ │ ├── ghcorner.component.html │ │ ├── ghcorner.component.spec.ts │ │ └── ghcorner.component.ts │ ├── screen │ │ ├── screen.component.css │ │ ├── screen.component.html │ │ ├── screen.component.spec.ts │ │ └── screen.component.ts │ └── stepper │ │ ├── stepper.component.css │ │ ├── stepper.component.html │ │ ├── stepper.component.spec.ts │ │ └── stepper.component.ts ├── assets │ ├── .gitkeep │ ├── globe-bk.png │ ├── phone-bk.jpg │ └── phone-bk.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "pwa-chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Signals Demo 2 | 3 | This little explainer serves as a visual explainer for some of the concepts Angular is interested in discussing with the introduction of Signals-based reactivity. Though Signals are not a new concept in the framework space, these differ radically both in terms of public API as well as behavioral details (such as eager vs lazy computation, batching behavior, equality, cleanup, nesting, etc). 4 | 5 | Demo site: [https://angular-signals.netlify.app/](https://angular-signals.netlify.app/) 6 | 7 | If you're interested in being part of the discussion, please join us in our discussion space for our RFC! 8 | 9 | [https://github.com/angular/angular/discussions/49090](https://github.com/angular/angular/discussions/49090) 10 | 11 | ![Angular Signals Demo](https://i.ibb.co/tKfJ7WK/signals-og.jpg) 12 | 13 | And here is a stackblitz that allows you to [play around with this initial version](https://stackblitz.com/edit/angular-ednkcj?file=src%2Fmain.ts), compliments to Enea Jahollari. 14 | 15 | 16 | 17 | 18 | 19 | --- 20 | 21 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.0-rc.2. 22 | 23 | ## Development server 24 | 25 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 26 | 27 | ## Code scaffolding 28 | 29 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 30 | 31 | ## Build 32 | 33 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 34 | 35 | ## Running unit tests 36 | 37 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 38 | 39 | ## Running end-to-end tests 40 | 41 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 42 | 43 | ## Further help 44 | 45 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 46 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "signals-demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:application": { 10 | "strict": true 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/signals-demo", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "@angular/material/prebuilt-themes/pink-bluegrey.css", 31 | "src/styles.css" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "budgets": [ 38 | { 39 | "type": "initial", 40 | "maximumWarning": "500kb", 41 | "maximumError": "1mb" 42 | }, 43 | { 44 | "type": "anyComponentStyle", 45 | "maximumWarning": "2kb", 46 | "maximumError": "4kb" 47 | } 48 | ], 49 | "fileReplacements": [ 50 | { 51 | "replace": "src/environments/environment.ts", 52 | "with": "src/environments/environment.prod.ts" 53 | } 54 | ], 55 | "outputHashing": "all" 56 | }, 57 | "development": { 58 | "buildOptimizer": false, 59 | "optimization": false, 60 | "vendorChunk": true, 61 | "extractLicenses": false, 62 | "sourceMap": true, 63 | "namedChunks": true 64 | } 65 | }, 66 | "defaultConfiguration": "production" 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "signals-demo:build:production" 73 | }, 74 | "development": { 75 | "browserTarget": "signals-demo:build:development" 76 | } 77 | }, 78 | "defaultConfiguration": "development" 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "signals-demo:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "polyfills": "src/polyfills.ts", 91 | "tsConfig": "tsconfig.spec.json", 92 | "karmaConfig": "karma.conf.js", 93 | "assets": [ 94 | "src/favicon.ico", 95 | "src/assets" 96 | ], 97 | "styles": [ 98 | "@angular/material/prebuilt-themes/pink-bluegrey.css", 99 | "src/styles.css" 100 | ], 101 | "scripts": [] 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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/signals-demo'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signals-demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^15.1.5", 14 | "@angular/cdk": "^15.1.5", 15 | "@angular/common": "^15.1.5", 16 | "@angular/compiler": "^15.1.5", 17 | "@angular/core": "^15.1.5", 18 | "@angular/forms": "^15.1.5", 19 | "@angular/material": "^15.1.5", 20 | "@angular/platform-browser": "^15.1.5", 21 | "@angular/platform-browser-dynamic": "^15.1.5", 22 | "@angular/router": "^15.1.5", 23 | "gsap": "^3.11.4", 24 | "rxjs": "~7.5.0", 25 | "tslib": "^2.3.0", 26 | "zone.js": "~0.11.4" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^15.1.6", 30 | "@angular/cli": "~15.1.6", 31 | "@angular/compiler-cli": "^15.1.5", 32 | "@types/jasmine": "~4.0.0", 33 | "jasmine-core": "~4.1.0", 34 | "karma": "~6.3.0", 35 | "karma-chrome-launcher": "~3.1.0", 36 | "karma-coverage": "~2.2.0", 37 | "karma-jasmine": "~5.0.0", 38 | "karma-jasmine-html-reporter": "~1.7.0", 39 | "typescript": "~4.9.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-template-columns: 1fr 3fr; 4 | grid-template-rows: 100px 1fr; 5 | grid-column-gap: 100px; 6 | grid-row-gap: 0px; 7 | max-width: 1100px; 8 | margin: 20px auto; 9 | } 10 | 11 | .header-area { grid-area: 1 / 1 / 2 / 3; text-align: center;} 12 | .screen-area { grid-area: 2 / 1 / 3 / 2; } 13 | .explain-area { grid-area: 2 / 2 / 3 / 3; } 14 | 15 | @media (max-width: 1100px) { 16 | .container { 17 | grid-template-columns: 1fr; 18 | grid-template-rows: 60px repeat(2, 1fr); 19 | grid-column-gap: 0px; 20 | grid-row-gap: 20px; 21 | } 22 | 23 | .header-area { grid-area: 1 / 1 / 2 / 2; } 24 | .screen-area { grid-area: 2 / 1 / 3 / 2; margin-left: -200px; height: 300px; } 25 | .explain-area { grid-area: 3 / 1 / 4 / 2; } 26 | 27 | .container h1 { 28 | font-size: 30px; 29 | } 30 | } 31 | 32 | @media (max-width: 600px) { 33 | .explain-area { margin-top: -100px; } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Angular Signals Demo

5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | }); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'signals-demo'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.componentInstance; 22 | expect(app.title).toEqual('signals-demo'); 23 | }); 24 | 25 | it('should render title', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.nativeElement as HTMLElement; 29 | expect(compiled.querySelector('.content span')?.textContent).toContain('signals-demo app is running!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'signals-demo'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { ScreenComponent } from './screen/screen.component'; 7 | import { MatStepperModule } from '@angular/material/stepper'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { StepperComponent } from './stepper/stepper.component'; 10 | import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper'; 11 | import { GhcornerComponent } from './ghcorner/ghcorner.component'; 12 | 13 | import { AppService } from './app.service'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | AppComponent, 18 | ScreenComponent, 19 | StepperComponent, 20 | GhcornerComponent 21 | ], 22 | imports: [ 23 | BrowserModule, 24 | FormsModule, 25 | MatStepperModule, 26 | BrowserAnimationsModule 27 | ], 28 | providers: [{ 29 | provide: STEPPER_GLOBAL_OPTIONS, 30 | useValue: { displayDefaultIndicatorType: false } 31 | }, AppService], 32 | bootstrap: [AppComponent] 33 | }) 34 | export class AppModule { } 35 | -------------------------------------------------------------------------------- /src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, Subject, of } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Injectable() 6 | export class AppService { 7 | 8 | public stringSubject = new Subject(); 9 | 10 | passValue(data:number) { 11 | //passing the data as the next observable 12 | this.stringSubject.next(data); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/app/ghcorner/ghcorner.component.css: -------------------------------------------------------------------------------- 1 | .github-corner:hover .octo-arm { 2 | animation: octocat-wave 560ms ease-in-out; 3 | } 4 | @keyframes octocat-wave { 5 | 0%, 6 | 100% { 7 | transform: rotate(0); 8 | } 9 | 20%, 10 | 60% { 11 | transform: rotate(-25deg); 12 | } 13 | 40%, 14 | 80% { 15 | transform: rotate(10deg); 16 | } 17 | } 18 | @media (max-width: 500px) { 19 | .github-corner:hover .octo-arm { 20 | animation: none; 21 | } 22 | .github-corner .octo-arm { 23 | animation: octocat-wave 560ms ease-in-out; 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/ghcorner/ghcorner.component.html: -------------------------------------------------------------------------------- 1 | 6 | 26 | 27 | -------------------------------------------------------------------------------- /src/app/ghcorner/ghcorner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GhcornerComponent } from './ghcorner.component'; 4 | 5 | describe('GhcornerComponent', () => { 6 | let component: GhcornerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ GhcornerComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(GhcornerComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/ghcorner/ghcorner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-ghcorner', 5 | templateUrl: './ghcorner.component.html', 6 | styleUrls: ['./ghcorner.component.css'] 7 | }) 8 | export class GhcornerComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/screen/screen.component.css: -------------------------------------------------------------------------------- 1 | .screen { 2 | transform: perspective(400px) rotateY(20deg); 3 | position: relative; 4 | width: 300px; 5 | height: 300px; 6 | } 7 | 8 | @media (max-width: 1100px) { 9 | .screen { 10 | width: 100px; 11 | margin: auto; 12 | } 13 | } 14 | 15 | .screen-svg { 16 | position: absolute; 17 | width: 350px; 18 | margin-top: 30px; 19 | } 20 | 21 | .screen-png { 22 | position: absolute; 23 | border: 1px solid #515050; 24 | } 25 | 26 | 27 | .cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7 { 28 | fill: none; 29 | } 30 | 31 | .cls-8 { 32 | clip-path: url(#clippath); 33 | } 34 | 35 | .cls-9, .cls-10 { 36 | fill: #fa2256; 37 | } 38 | 39 | .cls-11, .cls-12, .cls-13, .cls-14, .cls-15 { 40 | fill: #fff; 41 | } 42 | 43 | .cls-16, .cls-17 { 44 | fill: #30e0a1; 45 | } 46 | 47 | .cls-18, .cls-19 { 48 | fill: #11cabe; 49 | } 50 | 51 | .cls-20 { 52 | fill: #246cf9; 53 | } 54 | 55 | .cls-21 { 56 | fill: #1e1f25; 57 | } 58 | 59 | .cls-22 { 60 | fill: #3c4254; 61 | } 62 | 63 | .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7 { 64 | stroke-dasharray: 0 0 0 0; 65 | } 66 | 67 | .cls-2, .cls-6 { 68 | stroke: #fa2256; 69 | } 70 | 71 | .cls-3 { 72 | stroke: #3c4254; 73 | } 74 | 75 | .cls-4, .cls-5 { 76 | stroke: #246cf9; 77 | } 78 | 79 | .cls-5, .cls-6 { 80 | stroke-width: .5px; 81 | } 82 | 83 | .cls-12 { 84 | font-size: 12px; 85 | } 86 | 87 | .cls-12, .cls-13, .cls-10, .cls-14, .cls-19, .cls-17, .cls-15 { 88 | font-family: Helvetica, Helvetica; 89 | } 90 | 91 | .cls-12, .cls-13, .cls-10, .cls-14, .cls-19, .cls-17, .cls-15, .cls-23 { 92 | isolation: isolate; 93 | } 94 | 95 | .cls-13 { 96 | font-size: 36px; 97 | } 98 | 99 | .cls-10, .cls-14, .cls-19 { 100 | font-size: 14px; 101 | } 102 | 103 | .cls-24 { 104 | opacity: .6; 105 | } 106 | 107 | .cls-25 { 108 | opacity: .5; 109 | } 110 | 111 | .cls-17 { 112 | font-size: 13px; 113 | } 114 | 115 | .cls-15 { 116 | font-size: 16px; 117 | } 118 | 119 | .cls-7 { 120 | stroke: #48a1d9; 121 | stroke-linecap: round; 122 | stroke-linejoin: round; 123 | } 124 | 125 | /* animation starter */ 126 | 127 | #key-bk, #thismonth, #lastmonth, #keybk, .dot, .label { 128 | opacity: 0; 129 | } 130 | -------------------------------------------------------------------------------- /src/app/screen/screen.component.html: -------------------------------------------------------------------------------- 1 |
2 | phone image background 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | All Users 11 | 12 | {{ userNum }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 45 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | +15.4% 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | +0.08% 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -0.32% 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Last Month 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | {{monthNum}}% 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | This Month 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 1.52% 147 | 148 | 149 | 150 | 151 | 152 | 153 |
-------------------------------------------------------------------------------- /src/app/screen/screen.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ScreenComponent } from './screen.component'; 4 | 5 | describe('ScreenComponent', () => { 6 | let component: ScreenComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ScreenComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ScreenComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/screen/screen.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; 2 | import { AppService } from './../app.service'; 3 | import { gsap } from 'gsap'; 4 | 5 | @Component({ 6 | selector: 'app-screen', 7 | templateUrl: './screen.component.html', 8 | styleUrls: ['./screen.component.css'] 9 | }) 10 | export class ScreenComponent implements AfterViewInit { 11 | 12 | opener = gsap.timeline({delay: 2.5}); 13 | dur:number = 2 14 | path:number = 0 15 | stepIndex:number = 0 16 | userNum:number = 159845 17 | monthNum:number = 2.36 18 | 19 | constructor(private appService: AppService) { 20 | } 21 | 22 | @ViewChild("lines", {read: ElementRef}) private lines: ElementRef; 23 | 24 | ngAfterViewInit() { 25 | this.path = this.lines.nativeElement.getTotalLength() 26 | 27 | this.createOpener() 28 | 29 | this.appService.stringSubject.subscribe(data => { 30 | this.stepIndex = data; 31 | 32 | if (this.stepIndex === 1) this.createStep2() 33 | if (this.stepIndex === 2) this.createStep3() 34 | if (this.stepIndex === 3) this.createStep4() 35 | }); 36 | } 37 | 38 | createOpener() { 39 | gsap.set("#lines", { 40 | strokeDashoffset: `${this.path}px`, 41 | strokeDasharray: `${this.path}px` 42 | }) 43 | 44 | this.opener.to(".label", { 45 | duration: this.dur, 46 | opacity: 1, 47 | stagger: 0.5, 48 | ease: "sine.out" 49 | }, 1) 50 | this.opener.to(".dot", { 51 | duration: this.dur, 52 | opacity: 0.5, 53 | stagger: 0.5, 54 | ease: "sine.out" 55 | }, 2) 56 | this.opener.to("#keybk", { 57 | duration: this.dur, 58 | opacity: 1, 59 | ease: "sine.out" 60 | }, 4) 61 | this.opener.to("#lastmonth", { 62 | duration: this.dur, 63 | opacity: 1, 64 | ease: "sine.out" 65 | }, 4.5) 66 | this.opener.to("#thismonth", { 67 | duration: this.dur, 68 | opacity: 1, 69 | ease: "sine.out" 70 | }, 5) 71 | this.opener.to("#lines", { 72 | duration: 1.5, 73 | strokeDashoffset: 0 74 | }, 4) 75 | } 76 | 77 | createStep2() { 78 | setTimeout(() => { 79 | this.userNum = 160000 80 | 81 | gsap.to("#num", { 82 | duration: 0.5, 83 | textShadow: "#FFF 0px 0px 5px, #FFF 0px 0px 10px, #FFF 0px 0px 15px, #FF2D95 0px 0px 20px, #FF2D95 0px 0px 30px, #FF2D95 0px 0px 40px, #FF2D95 0px 0px 50px, #FF2D95 0px 0px 75px", 84 | }) 85 | gsap.to("#num", { 86 | duration: 1, 87 | delay: 3.5, 88 | textShadow: "1px 1px 1px rgba(0, 0, 0, 0)", 89 | }) 90 | 91 | }, 3000) 92 | 93 | setTimeout(() => { 94 | this.userNum = 160001 95 | }, 5000) 96 | } 97 | 98 | createStep3() { 99 | setTimeout(() => { 100 | this.monthNum = 2.45 101 | 102 | gsap.to("#month", { 103 | duration: 0.5, 104 | textShadow: "0 0 5px #FFF, 0 0 10px #FFF, 0 0 15px #FFF, 0 0 20px #49ff18, 0 0 30px #49FF18, 0 0 40px #49FF18, 0 0 55px #49FF18, 0 0 75px #49ff18", 105 | }) 106 | gsap.to("#month", { 107 | duration: 1, 108 | delay: 3.5, 109 | textShadow: "1px 1px 1px rgba(0, 0, 0, 0)", 110 | }) 111 | 112 | }, 3000) 113 | } 114 | 115 | createStep4() { 116 | gsap.set("#lines", { 117 | strokeDashoffset: `${this.path}px`, 118 | strokeDasharray: `${this.path}px` 119 | }) 120 | 121 | gsap.to("#lines", { 122 | duration: 2, 123 | delay: 4, 124 | strokeDashoffset: 0 125 | }) 126 | } 127 | 128 | } 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/app/stepper/stepper.component.css: -------------------------------------------------------------------------------- 1 | .mat-horizontal-content-container { 2 | height: 100%; 3 | } 4 | .mat-horizontal-stepper-content { 5 | height: 100%; 6 | display: flex; 7 | } -------------------------------------------------------------------------------- /src/app/stepper/stepper.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

Let's say you have an app like this one.

6 |

We want to set up any data we might like to track with a signal. A signal is a value which is "reactive", meaning it can notify interested consumers when it changes.

7 |

(() => T)

8 |

Signal are zero-argument functions. When executed, they return the current value of the signal.

9 |

In this particular app, we might want to track the users, listed here at the top.

10 |

users = signal(159845);

11 |
12 |
13 | 14 | 15 |
16 |

Let's say you want to update our users, we know that as of our last update, we now have 160000. We can set that amount.

17 |

users = signal(159845);
18 |
19 | users.set(160000);

20 |

We can also update the users as new users come on to the platform.

21 |

users.update(user => user + 1);

22 |

We do this via SettableSignal. In addition to being a getter function, we can also change the value of the signal, and notify any dependents of the change. The API includes .set for replacement, .update for deriving a new value, and .mutate for performing internal mutation of the value.

23 |
24 |
25 | 26 | 27 |
28 |

There are a few amounts on this page derived from our users. In the key at the bottom, you can see the change between this month and last month.

29 |

We don't necessarily want a whole new value to update manually. Computed values allow us to show a derived value, a new view from our data in our signal.

30 |

// Automatically updates when `users` changes:
31 | const thisMonth = computed(() => users() - lastMonth);

32 |

Computed values are cached based on their dependencies and will only update when necessary.

33 |

Any signals read by that calculation will be tracked as dependencies, and the value recalculated when any of these dependencies change.

34 |
35 |
36 | 37 | 38 |
39 |

There are times when we might want something to occur when a signal has a new value.

40 |

With effect(), we can schedule and run a side-effectful function inside a reactive context.

41 |

Let's say we want to redraw the connecting lines when a user amount changes, in case the lines are adjusting.

42 |

43 | users.set(180000);
44 | effect(() => redrawLines()); 45 |

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

This is just the beginning! 🎉

52 |

Angular Signals also has:

53 |
    54 |
  • Flexible effect scheduling, allowing for seamless integration with Angular, and other adjacent libraries like RxJS
  • 55 |
  • Leaning into the compiler to optimize various reactive operations, especially DOM APIs and updates
  • 56 |
  • Lazy evaluation, so that the user doesn't have to explicitly batch operations
  • 57 |
58 |

Head over to the RFC for further updates and discussion! >

59 |
60 |
61 | 62 |
63 | -------------------------------------------------------------------------------- /src/app/stepper/stepper.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { StepperComponent } from './stepper.component'; 4 | 5 | describe('StepperComponent', () => { 6 | let component: StepperComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ StepperComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(StepperComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/stepper/stepper.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterViewInit } from '@angular/core'; 2 | import { AppService } from './../app.service'; 3 | import { gsap } from 'gsap'; 4 | 5 | @Component({ 6 | selector: 'app-stepper', 7 | templateUrl: './stepper.component.html', 8 | styleUrls: ['./stepper.component.css'] 9 | }) 10 | export class StepperComponent implements AfterViewInit { 11 | 12 | stepText = gsap.timeline(); 13 | stepIndex:number = 0 14 | 15 | constructor(private appService: AppService) { 16 | } 17 | 18 | ngAfterViewInit() { 19 | this.stepAnim(0) 20 | } 21 | 22 | onStepChange(event: any): void { 23 | console.log(event.selectedIndex); 24 | this.stepIndex = event.selectedIndex; 25 | this.appService.passValue(event.selectedIndex); 26 | 27 | this.stepText.restart() 28 | this.stepAnim(this.stepIndex) 29 | } 30 | 31 | stepAnim(stepIndex:number) { 32 | this.stepText.fromTo(`.step${stepIndex + 1} p, .step${stepIndex + 1} li`, { 33 | opacity: 0 34 | }, 35 | { 36 | duration: 2, 37 | opacity: 1, 38 | stagger: 0.3, 39 | ease: "sine.out" 40 | }, 1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/signals-demo/b8450e2f053a9684e09a07e18b0e5f778b7287ed/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/globe-bk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/signals-demo/b8450e2f053a9684e09a07e18b0e5f778b7287ed/src/assets/globe-bk.png -------------------------------------------------------------------------------- /src/assets/phone-bk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/signals-demo/b8450e2f053a9684e09a07e18b0e5f778b7287ed/src/assets/phone-bk.jpg -------------------------------------------------------------------------------- /src/assets/phone-bk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/signals-demo/b8450e2f053a9684e09a07e18b0e5f778b7287ed/src/assets/phone-bk.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/signals-demo/b8450e2f053a9684e09a07e18b0e5f778b7287ed/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SignalsDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/prebuilt-themes/pink-bluegrey.css'; 3 | 4 | html, body { height: 100%; } 5 | 6 | body { 7 | font-family: 'Jost', sans-serif; 8 | background: #000; 9 | color: white; 10 | } 11 | 12 | button { 13 | padding: 10px; 14 | border: 1px solid black; 15 | margin: 5px; 16 | height: 35px; 17 | cursor: pointer; 18 | font-family: 'Jost', sans-serif; 19 | } 20 | 21 | text { 22 | font-family: 'Jost', sans-serif; 23 | } 24 | 25 | h1 { 26 | color: #7fa3ee; 27 | } 28 | 29 | p, li { 30 | font-size: 19px; 31 | font-weight: 400; 32 | letter-spacing: 0.02em; 33 | font-family: 'Jost', sans-serif; 34 | } 35 | 36 | .code { 37 | font-family: 'JetBrains Mono', monospace; 38 | background: #222; 39 | border-radius: 4px; 40 | border: 1px solid #515050; 41 | padding: 10px; 42 | color: #c2e4f9; 43 | font-size: 15px; 44 | } 45 | 46 | code { 47 | font-family: 'JetBrains Mono', monospace; 48 | font-size: 15px; 49 | color: #c2e4f9; 50 | } 51 | 52 | .mat-stepper-horizontal, .mat-stepper-vertical { 53 | background-color: transparent; 54 | } 55 | 56 | a, a:visited { 57 | color: #7fa3ee; 58 | text-decoration: none; 59 | } 60 | 61 | .emphasis { 62 | color: #eeba7f; 63 | } 64 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /tsconfig.app.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/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ], 25 | "useDefineForClassFields": false, 26 | "strictPropertyInitialization": false, 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------