├── .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 | 
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 |
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 |
13 |
14 |
20 |
25 |
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 |
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 |
--------------------------------------------------------------------------------