├── .editorconfig ├── .babelrc ├── .eslintignore ├── first-ng-app ├── src │ ├── app │ │ ├── components │ │ │ ├── counter │ │ │ │ ├── counter.component.scss │ │ │ │ ├── counter.component.html │ │ │ │ ├── counter.component.ts │ │ │ │ └── counter.component.spec.ts │ │ │ ├── greeting │ │ │ │ ├── greeting.component.scss │ │ │ │ ├── greeting.component.html │ │ │ │ ├── greeting.component.ts │ │ │ │ └── greeting.component.spec.ts │ │ │ ├── todo-item │ │ │ │ ├── todo-item.component.scss │ │ │ │ ├── todo-item.component.html │ │ │ │ ├── todo-item.component.spec.ts │ │ │ │ └── todo-item.component.ts │ │ │ └── header │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.ts │ │ │ │ ├── header.component.scss │ │ │ │ └── header.component.spec.ts │ │ ├── home │ │ │ ├── home.component.scss │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── home.component.spec.ts │ │ ├── model │ │ │ └── todo.type.ts │ │ ├── todos │ │ │ ├── todos.component.scss │ │ │ ├── todos.component.html │ │ │ ├── todos.component.spec.ts │ │ │ └── todos.component.ts │ │ ├── pipes │ │ │ ├── filter-todos.pipe.spec.ts │ │ │ └── filter-todos.pipe.ts │ │ ├── directives │ │ │ ├── highlight-completed-todo.directive.spec.ts │ │ │ └── highlight-completed-todo.directive.ts │ │ ├── services │ │ │ ├── todos.service.spec.ts │ │ │ └── todos.service.ts │ │ ├── app.routes.ts │ │ ├── app.config.ts │ │ ├── app.component.ts │ │ └── app.component.spec.ts │ ├── styles.scss │ ├── main.ts │ └── index.html ├── public │ └── favicon.ico ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── angular.json ├── postcss.config.js ├── .npmignore ├── assets └── images │ ├── banner.png │ ├── gde-logo.png │ ├── code with ahsan.png │ ├── ng-cookbook-2.png │ ├── mastering-angular-signals.png │ ├── app-mockup.svg │ ├── components-1.svg │ ├── home-with-nested-components.svg │ ├── todos-route.svg │ └── todo-item.svg ├── data └── slides.json ├── .gitignore ├── .prettierrc ├── tailwind.config.js ├── .eslintrc.json ├── content ├── my-slides.md ├── slides.html ├── static-slides.html └── angular-in-90ish.md ├── js └── slides.js ├── main.js ├── .github ├── CONTRIBUTING.md └── workflows │ └── deploy.yml ├── LICENSE ├── index.html ├── scripts ├── addIdsToSlide.js └── extractSlideData.js ├── profiles └── ahsan.md ├── README.md ├── css └── slides.scss ├── package.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | { 2 | printLength: 80 3 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | js/controllers/ 2 | js/utils/ 3 | plugin -------------------------------------------------------------------------------- /first-ng-app/src/app/components/counter/counter.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/greeting/greeting.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/todo-item/todo-item.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /first-ng-app/src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | input { 2 | margin-top: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /examples 3 | .github 4 | .gulpfile 5 | .sass-cache 6 | gulpfile.js 7 | CONTRIBUTING.md -------------------------------------------------------------------------------- /assets/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/banner.png -------------------------------------------------------------------------------- /assets/images/gde-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/gde-logo.png -------------------------------------------------------------------------------- /first-ng-app/src/app/components/greeting/greeting.component.html: -------------------------------------------------------------------------------- 1 |

Greetings!

2 |

{{ message() }}

3 | -------------------------------------------------------------------------------- /assets/images/code with ahsan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/code with ahsan.png -------------------------------------------------------------------------------- /assets/images/ng-cookbook-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/ng-cookbook-2.png -------------------------------------------------------------------------------- /data/slides.json: -------------------------------------------------------------------------------- 1 | [{"link":"angular-in-90ish.md","title":"Angular in 90-ish minutes"},{"link":"my-slides.md","title":"My Slides"}] -------------------------------------------------------------------------------- /first-ng-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/first-ng-app/public/favicon.ico -------------------------------------------------------------------------------- /assets/images/mastering-angular-signals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/mastering-angular-signals.png -------------------------------------------------------------------------------- /first-ng-app/src/app/model/todo.type.ts: -------------------------------------------------------------------------------- 1 | export type Todo = { 2 | userId: number; 3 | completed: boolean; 4 | title: string; 5 | id: number; 6 | }; 7 | -------------------------------------------------------------------------------- /first-ng-app/src/app/todos/todos.component.scss: -------------------------------------------------------------------------------- 1 | .todos { 2 | &__item { 3 | display: flex; 4 | align-items: center; 5 | gap: 8px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | *.iws 4 | *.eml 5 | out/ 6 | .DS_Store 7 | .svn 8 | log/*.log 9 | tmp/** 10 | node_modules/ 11 | .sass-cache 12 | build 13 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /first-ng-app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./content/**/*.{html,js,css}', './css/**/*.scss'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /first-ng-app/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | -------------------------------------------------------------------------------- /first-ng-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | * { 10 | outline: none; 11 | } 12 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/counter/counter.component.html: -------------------------------------------------------------------------------- 1 |

Counter value: {{ counterValue() }}

2 | 3 |
4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /first-ng-app/src/app/pipes/filter-todos.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FilterTodosPipe } from './filter-todos.pipe'; 2 | 3 | describe('FilterTodosPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FilterTodosPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /first-ng-app/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 | -------------------------------------------------------------------------------- /first-ng-app/src/app/directives/highlight-completed-todo.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { HighlightCompletedTodoDirective } from './highlight-completed-todo.directive'; 2 | 3 | describe('HighlightCompletedTodoDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new HighlightCompletedTodoDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/greeting/greeting.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-greeting', 5 | standalone: true, 6 | imports: [], 7 | templateUrl: './greeting.component.html', 8 | styleUrl: './greeting.component.scss', 9 | }) 10 | export class GreetingComponent { 11 | message = input('Hello hello!'); 12 | } 13 | -------------------------------------------------------------------------------- /first-ng-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FirstNgApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /first-ng-app/.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 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/todo-item/todo-item.component.html: -------------------------------------------------------------------------------- 1 |
  • 6 | 12 | 13 |
  • 14 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './header.component.html', 9 | styleUrl: './header.component.scss', 10 | }) 11 | export class HeaderComponent { 12 | title = 'My first Angular app'; 13 | } 14 | -------------------------------------------------------------------------------- /first-ng-app/src/app/services/todos.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TodosService } from './todos.service'; 4 | 5 | describe('TodosService', () => { 6 | let service: TodosService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TodosService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /first-ng-app/src/app/services/todos.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { Todo } from '../model/todo.type'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class TodosService { 9 | http = inject(HttpClient); 10 | getTodosFromApi() { 11 | const url = `https://jsonplaceholder.typicode.com/todos`; 12 | return this.http.get>(url); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /first-ng-app/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | pathMatch: 'full', 7 | loadComponent: () => { 8 | return import('./home/home.component').then((m) => m.HomeComponent); 9 | }, 10 | }, 11 | { 12 | path: 'todos', 13 | loadComponent: () => { 14 | return import('./todos/todos.component').then((m) => m.TodosComponent); 15 | }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /first-ng-app/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideHttpClient } from '@angular/common/http'; 6 | 7 | export const appConfig: ApplicationConfig = { 8 | providers: [ 9 | provideHttpClient(), 10 | provideZoneChangeDetection({ eventCoalescing: true }), 11 | provideRouter(routes), 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /first-ng-app/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 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /first-ng-app/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 | -------------------------------------------------------------------------------- /first-ng-app/src/app/todos/todos.component.html: -------------------------------------------------------------------------------- 1 |

    Todos List

    2 | 3 | @if (!todoItems().length) { 4 |

    Loading...

    5 | } 6 | 7 |
    8 | 9 | 14 |
    15 | 16 |
      17 | @for (todo of todoItems() | filterTodos : searchTerm(); track todo.id) { 18 | 19 | } 20 |
    21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": "latest", // or a version high enough to support ES6+ 4 | "sourceType": "module", // Allows use of imports 5 | "ecmaFeatures": { 6 | "jsx": true // if you are using React 7 | } 8 | }, 9 | "env": { 10 | "browser": true, 11 | "es2021": true 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | // Add react presets if you're using React 16 | "plugin:react/recommended" 17 | ], 18 | "rules": { 19 | // Your custom rules 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /first-ng-app/src/app/pipes/filter-todos.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Todo } from '../model/todo.type'; 3 | 4 | @Pipe({ 5 | name: 'filterTodos', 6 | standalone: true, 7 | }) 8 | export class FilterTodosPipe implements PipeTransform { 9 | transform(todos: Todo[], searchTerm: string): Todo[] { 10 | if (!searchTerm) { 11 | return todos; 12 | } 13 | const text = searchTerm.toLowerCase(); 14 | return todos.filter((todo) => { 15 | return todo.title.toLowerCase().includes(text); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /first-ng-app/.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": "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 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/counter/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, signal } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-counter', 5 | standalone: true, 6 | imports: [], 7 | templateUrl: './counter.component.html', 8 | styleUrl: './counter.component.scss', 9 | }) 10 | export class CounterComponent { 11 | counterValue = signal(0); 12 | increment() { 13 | this.counterValue.update((val) => val + 1); 14 | } 15 | 16 | decrement() { 17 | this.counterValue.update((val) => val - 1); 18 | } 19 | 20 | reset() { 21 | this.counterValue.set(0); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | import { HeaderComponent } from './components/header/header.component'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | standalone: true, 8 | imports: [RouterOutlet, HeaderComponent], 9 | template: ` 10 | 11 |
    12 | 13 |
    14 | `, 15 | styles: [ 16 | ` 17 | main { 18 | padding: 16px; 19 | } 20 | `, 21 | ], 22 | }) 23 | export class AppComponent { 24 | title = 'first-ng-app'; 25 | } 26 | -------------------------------------------------------------------------------- /first-ng-app/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, signal } from '@angular/core'; 2 | import { GreetingComponent } from '../components/greeting/greeting.component'; 3 | import { CounterComponent } from '../components/counter/counter.component'; 4 | 5 | @Component({ 6 | selector: 'app-home', 7 | standalone: true, 8 | imports: [GreetingComponent, CounterComponent], 9 | templateUrl: './home.component.html', 10 | styleUrl: './home.component.scss', 11 | }) 12 | export class HomeComponent { 13 | homeMessage = signal('Hello, world!'); 14 | 15 | keyUpHandler(event: KeyboardEvent) { 16 | console.log(`user pressed the ${event.key} key`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /content/my-slides.md: -------------------------------------------------------------------------------- 1 | # My Slides 2 | 3 | Here goes some content in first 4 | 5 | -- 6 | 7 | ## Second Slide (vertical) 8 | 9 | more content 10 | 11 | --- 12 | 13 | ## Horizontal slide 14 | ### (horizontal separated) 15 | 16 | Yeah, that can happen 17 | 18 | -- 19 | 20 | This is some code 21 | 22 | ```js 23 | const foo = 'bar'; 24 | ``` 25 | 26 | Code step highlighting 27 | 28 | ```ts [1-3] 29 | const main = async () => { 30 | const resp = await fetch('data/slides.json'); 31 | const slides = await resp.json(); 32 | const grid = document.getElementById('talksGrid'); 33 | slides.forEach((slide) => { 34 | // ... 35 | }); 36 | }; 37 | 38 | main(); 39 | 40 | 41 | ``` -------------------------------------------------------------------------------- /js/slides.js: -------------------------------------------------------------------------------- 1 | import Reveal from 'reveal.js'; 2 | import 'reveal.js/dist/reveal.css'; 3 | import 'reveal.js/dist/theme/black.css'; 4 | import 'reveal.js/plugin/highlight/monokai.css'; 5 | import * as RevealHighlight from 'reveal.js/plugin/highlight/highlight.js'; 6 | import * as RevealMarkdown from 'reveal.js/plugin/markdown/markdown.js'; 7 | import * as RevealNotes from 'reveal.js/plugin/notes/notes.js'; 8 | import * as RevealMath from 'reveal.js/plugin/math/math.js'; 9 | import '../css/slides.scss'; 10 | 11 | Reveal.initialize({ 12 | controls: true, 13 | progress: true, 14 | history: true, 15 | center: true, 16 | 17 | plugins: [RevealMarkdown, RevealHighlight, RevealNotes, RevealMath.KaTeX], 18 | }); 19 | -------------------------------------------------------------------------------- /first-ng-app/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HomeComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HomeComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/header/header.component.scss: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | padding-inline: 16px; 4 | padding-block: 8px; 5 | background-color: #333; 6 | color: white; 7 | align-items: center; 8 | justify-content: space-between; 9 | 10 | nav { 11 | width: 100%; 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | 16 | > span { 17 | cursor: pointer; 18 | &:hover { 19 | color: #777; 20 | } 21 | } 22 | 23 | ul { 24 | list-style: none; 25 | 26 | li { 27 | cursor: pointer; 28 | &:hover { 29 | color: #777; 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /first-ng-app/src/app/todos/todos.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TodosComponent } from './todos.component'; 4 | 5 | describe('TodosComponent', () => { 6 | let component: TodosComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [TodosComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TodosComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HeaderComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HeaderComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/counter/counter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CounterComponent } from './counter.component'; 4 | 5 | describe('CounterComponent', () => { 6 | let component: CounterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CounterComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CounterComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/greeting/greeting.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GreetingComponent } from './greeting.component'; 4 | 5 | describe('GreetingComponent', () => { 6 | let component: GreetingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [GreetingComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(GreetingComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/todo-item/todo-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TodoItemComponent } from './todo-item.component'; 4 | 5 | describe('TodoItemComponent', () => { 6 | let component: TodoItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [TodoItemComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TodoItemComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /first-ng-app/src/app/components/todo-item/todo-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input, output } from '@angular/core'; 2 | import { Todo } from '../../model/todo.type'; 3 | import { HighlightCompletedTodoDirective } from '../../directives/highlight-completed-todo.directive'; 4 | import { UpperCasePipe } from '@angular/common'; 5 | 6 | @Component({ 7 | selector: 'app-todo-item', 8 | standalone: true, 9 | imports: [HighlightCompletedTodoDirective, UpperCasePipe], 10 | templateUrl: './todo-item.component.html', 11 | styleUrl: './todo-item.component.scss', 12 | }) 13 | export class TodoItemComponent { 14 | todo = input.required(); 15 | todoToggled = output(); 16 | 17 | todoClicked() { 18 | this.todoToggled.emit(this.todo()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /first-ng-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 | -------------------------------------------------------------------------------- /first-ng-app/src/app/directives/highlight-completed-todo.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, input, effect, inject, ElementRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appHighlightCompletedTodo]', 5 | standalone: true, 6 | }) 7 | export class HighlightCompletedTodoDirective { 8 | isCompleted = input(false); 9 | el = inject(ElementRef); 10 | stylesEffect = effect(() => { 11 | if (this.isCompleted()) { 12 | this.el.nativeElement.style.textDecoration = 'line-through'; 13 | this.el.nativeElement.style.backgroundColor = '#d3f9d8'; 14 | this.el.nativeElement.style.color = '#6c757d'; 15 | } else { 16 | this.el.nativeElement.style.textDecoration = 'none'; 17 | this.el.nativeElement.style.backgroundColor = '#fff'; 18 | this.el.nativeElement.style.color = '#000'; 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const main = async () => { 2 | const resp = await fetch('data/slides.json'); 3 | const slides = await resp.json(); 4 | const grid = document.getElementById('talksGrid'); 5 | slides.forEach((slide) => { 6 | const { link, title } = slide; 7 | const sectionEl = document.createElement('section'); 8 | sectionEl.className = 9 | 'shadow-md border border-slate-300 rounded-md hover:bg-purple-700 duration-200 hover:text-white cursor-pointer'; 10 | const anchorEl = document.createElement('a'); 11 | anchorEl.className = 'p-4 w-full h-full block'; 12 | anchorEl.href = `slides.html?md=${encodeURIComponent( 13 | link 14 | )}&title=${encodeURIComponent(title)}`; 15 | anchorEl.target = '_blank'; 16 | anchorEl.textContent = title; 17 | sectionEl.appendChild(anchorEl); 18 | grid.appendChild(sectionEl); 19 | }); 20 | }; 21 | 22 | main(); 23 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Please keep the [issue tracker](https://github.com/ahsanayaz/angular-in-90ish/issues) limited to **bug reports**. 4 | 5 | ### General Questions and Support 6 | 7 | If you have questions about how to use reveal.js the best place to ask is in the [Discussions](https://github.com/ahsanayaz/angular-in-90ish/discussions). Anything that isn't a bug report should be posted as a dicussion instead. 8 | 9 | ### Bug Reports 10 | 11 | When reporting a bug make sure to include information about which browser and operating system you are on as well as the necessary steps to reproduce the issue. If possible please include a link to a sample presentation where the bug can be tested. 12 | 13 | ### Pull Requests 14 | 15 | - Should be submitted from a feature/topic branch (not your master) 16 | - Should follow the coding style of the file you work in, most importantly: 17 | - Tabs to indent 18 | - Single-quoted strings 19 | 20 | -------------------------------------------------------------------------------- /first-ng-app/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 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'first-ng-app' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('first-ng-app'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, first-ng-app'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /first-ng-app/.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2022 Hakim El Hattab, http://hakim.se, and reveal.js contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /first-ng-app/tsconfig.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 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /first-ng-app/README.md: -------------------------------------------------------------------------------- 1 | # FirstNgApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.7. 4 | 5 | ## Development server 6 | 7 | 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. 8 | 9 | ## Code scaffolding 10 | 11 | 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`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | 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. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 28 | -------------------------------------------------------------------------------- /first-ng-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "first-ng-app", 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": "^18.2.0", 14 | "@angular/common": "^18.2.0", 15 | "@angular/compiler": "^18.2.0", 16 | "@angular/core": "^18.2.0", 17 | "@angular/forms": "^18.2.0", 18 | "@angular/platform-browser": "^18.2.0", 19 | "@angular/platform-browser-dynamic": "^18.2.0", 20 | "@angular/router": "^18.2.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.14.10" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^18.2.7", 27 | "@angular/cli": "^18.2.7", 28 | "@angular/compiler-cli": "^18.2.0", 29 | "@types/jasmine": "~5.1.0", 30 | "jasmine-core": "~5.2.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.1.0", 36 | "typescript": "~5.5.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Slides - Code with Ahsan 10 | 11 | 12 | 16 | 17 | 23 | 24 | 25 |
    26 |

    Slides

    27 |
    28 |
    29 | 30 |
    31 | 32 | @codewith_ahsan 33 |
    34 | 35 | 36 | -------------------------------------------------------------------------------- /scripts/addIdsToSlide.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { v4: uuidv4 } = require('uuid'); 4 | const targetFile = process.argv[2]; 5 | 6 | if (!targetFile) { 7 | console.log('Error: target file not provided\nUse `npm run addIdsToSlide ./path/to/slides.md`'); 8 | process.exit(1); 9 | } 10 | 11 | const filePath = path.resolve(process.cwd(), targetFile); 12 | 13 | const addIdToSections = async (filePath) => { 14 | console.log(`Adding IDs to file ${filePath}\n`); 15 | try { 16 | let data = fs.readFileSync(filePath, 'utf8'); 17 | 18 | const separators = [';VS;', ';HS;']; 19 | 20 | separators.forEach(sep => { 21 | // Break into sections 22 | const sections = data.split(new RegExp(`\n${sep}[\r\n]`)); 23 | const updatedSections = sections.map(section => { 24 | if(!section.includes('\n${section}`; 26 | } 27 | // TODO: check duplicate IDs 28 | return section; 29 | }); 30 | 31 | // Join the sections back together 32 | data = updatedSections.join(`\n${sep}\n`); 33 | }); 34 | 35 | // Write the new data to file 36 | fs.writeFileSync(filePath, data, 'utf8'); 37 | console.log('IDs successfully added!'); 38 | } catch (err) { 39 | console.error('An error occurred:', err); 40 | } 41 | }; 42 | 43 | // Usage 44 | addIdToSections(filePath); 45 | -------------------------------------------------------------------------------- /profiles/ahsan.md: -------------------------------------------------------------------------------- 1 | ### Who Am I? 2 | 3 |
    4 |
    5 | 6 |
    7 |

    GDE in Angular

    8 | 9 |

    Software Architect at Scania Group

    10 |
    11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Angular in 90-ish minutes 2 | 3 | Ever wanted to learn Angular quickly? Well, this repository should help. This repository is created alongside the following video tutorial to teach the most important (& core) concepts of Angular: 4 | 5 | 6 | 7 | 8 | 9 | And if you're looking for a comprehensive guide to master Angular's new reactivity model, check out my book: 10 | 11 | 12 | 13 | 14 | 15 | > From a Google-Awarded Expert, this is your definitive guide to ending state management headaches and building lightning-fast Angular applications. 16 | 17 | The repository contains both the slides shown in the video, and the application we built during the video as well. 18 | 19 | ## Watching the slides 20 | 21 | The slides are deployed [here](https://ahsanayaz.github.io/angular-in-90ish/). 22 | 23 | ## Running the slides locally 24 | - Clone this repository 25 | - `npm install` 26 | - `npm run dev` 27 | 28 | 29 | ## Running the app 30 | 31 | To run the app we built in the video tutorial (the final state): 32 | - Clone this repository if you haven't 33 | - `cd first-ng-app` 34 | - `npm install` 35 | - `npm start` 36 | - Navigate to [localhost:4200](http://localhost:4200) 37 | -------------------------------------------------------------------------------- /first-ng-app/src/app/todos/todos.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, OnInit, signal } from '@angular/core'; 2 | import { TodosService } from '../services/todos.service'; 3 | import { Todo } from '../model/todo.type'; 4 | import { catchError } from 'rxjs'; 5 | import { TodoItemComponent } from '../components/todo-item/todo-item.component'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { FilterTodosPipe } from '../pipes/filter-todos.pipe'; 8 | 9 | @Component({ 10 | selector: 'app-todos', 11 | standalone: true, 12 | imports: [TodoItemComponent, FormsModule, FilterTodosPipe], 13 | templateUrl: './todos.component.html', 14 | styleUrl: './todos.component.scss', 15 | }) 16 | export class TodosComponent implements OnInit { 17 | todoService = inject(TodosService); 18 | todoItems = signal>([]); 19 | searchTerm = signal(''); 20 | 21 | ngOnInit(): void { 22 | this.todoService 23 | .getTodosFromApi() 24 | .pipe( 25 | catchError((err) => { 26 | console.log(err); 27 | throw err; 28 | }) 29 | ) 30 | .subscribe((todos) => { 31 | this.todoItems.set(todos); 32 | }); 33 | } 34 | 35 | updateTodoItem(todoItem: Todo) { 36 | this.todoItems.update((todos) => { 37 | return todos.map((todo) => { 38 | if (todo.id === todoItem.id) { 39 | return { 40 | ...todo, 41 | completed: !todo.completed, 42 | }; 43 | } 44 | return todo; 45 | }); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: 'pages' 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | cache: 'npm' 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Build 41 | run: npm run build 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v3 46 | with: 47 | # Upload dist folder 48 | path: './dist/angular-in-90ish' 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /content/slides.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reveal Multi Slides 7 | 8 | 9 | 13 | 14 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | 26 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /css/slides.scss: -------------------------------------------------------------------------------- 1 | .introduction { 2 | display: flex; 3 | margin-top: 60px; 4 | align-items: center; 5 | justify-content: space-between; 6 | gap: 30px; 7 | &__left { 8 | display: flex; 9 | gap: 16px; 10 | &__avatar { 11 | border-radius: 50%; 12 | width: 160px; 13 | height: 160px; 14 | } 15 | &__info { 16 | font-size: 24px; 17 | } 18 | } 19 | &__right { 20 | display: flex; 21 | gap: 16px; 22 | flex-direction: column; 23 | align-items: center; 24 | &__gde { 25 | width: 200px; 26 | } 27 | &__ng-book { 28 | width: 200px; 29 | } 30 | } 31 | } 32 | 33 | .footer { 34 | margin-top: 60px; 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: center; 38 | font-size: 24px; 39 | } 40 | 41 | img.meme { 42 | max-height: 600px !important; 43 | } 44 | 45 | .reveal .slide-number { 46 | position: absolute; 47 | display: block; 48 | right: 8px; 49 | bottom: 8px; 50 | z-index: 31; 51 | font-family: 'Nunito', Helvetica, sans-serif; 52 | font-size: 12px; 53 | line-height: 1; 54 | color: #fff; 55 | background-color: rgba(0, 0, 0, 0.4) !important; 56 | padding: 5px; 57 | } 58 | 59 | .reveal .slide-background { 60 | display: none; 61 | position: absolute; 62 | width: 100%; 63 | height: 100%; 64 | opacity: 0; 65 | visibility: hidden; 66 | overflow: hidden; 67 | 68 | background-color: rgba(0, 0, 0, 0); 69 | 70 | transition: all 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 71 | } 72 | 73 | :root { 74 | --r-background-color: #000; 75 | --r-main-font: 'Nunito', Helvetica, sans-serif; 76 | --r-heading-font: 'Nunito', Helvetica, sans-serif; 77 | --r-heading-text-transform: 'unset'; 78 | } 79 | 80 | .reveal li code { 81 | color: yellow; 82 | } 83 | -------------------------------------------------------------------------------- /content/static-slides.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Static Slides 7 | 8 | 9 | 10 | 14 | 15 | 16 |
    17 |
    18 |
    24 | 39 |
    40 |
    41 |
    42 | 43 | 44 | -------------------------------------------------------------------------------- /scripts/extractSlideData.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const listFilesInDirectory = (directory, fileNames) => { 5 | const files = fs.readdirSync(directory); 6 | files.forEach((file) => { 7 | const filePath = path.join(directory, file); 8 | if (fs.statSync(filePath).isDirectory()) { 9 | listFilesInDirectory(filePath, fileNames); 10 | } else { 11 | fileNames.push(filePath); 12 | } 13 | }); 14 | }; 15 | 16 | const extractTitleFromMarkdown = (path) => { 17 | try { 18 | const data = fs.readFileSync(path, 'utf8'); 19 | const headingRegex = /^#{1,2}\s+(.*)/; 20 | const lines = data.split('\n'); 21 | for (let line of lines) { 22 | const match = line.match(headingRegex); 23 | if (match) { 24 | return match[1].trim(); // Extracts the first Markdown heading 25 | } 26 | } 27 | return ''; // Return an empty string if no heading is found 28 | } catch (err) { 29 | console.log(err.message); 30 | process.exit(1); 31 | } 32 | }; 33 | 34 | const extractSlideData = (folderName) => { 35 | try { 36 | const talksPath = path.resolve(folderName); 37 | const files = fs.readdirSync(talksPath); 38 | 39 | const markdownFilter = /\.md$/; 40 | const folderFilter = /\./; 41 | 42 | let content = []; 43 | files.forEach((file) => { 44 | if (!folderFilter.test(file)) { 45 | const fileNames = []; 46 | listFilesInDirectory(path.join(talksPath, file), fileNames); 47 | 48 | return fileNames.forEach((file) => { 49 | if (markdownFilter.test(file)) { 50 | const title = extractTitleFromMarkdown(file); 51 | content.push({ 52 | link: file.replace(talksPath, '').substring(1), 53 | title, 54 | }); 55 | } 56 | }); 57 | } 58 | 59 | if (markdownFilter.test(file)) { 60 | const title = extractTitleFromMarkdown(path.join(talksPath, file)); 61 | content.push({ link: file, title }); 62 | } 63 | }); 64 | 65 | const jsonTalks = JSON.stringify(content); 66 | return jsonTalks; 67 | } catch (err) { 68 | console.log(err); 69 | process.exit(1); 70 | } 71 | }; 72 | 73 | const saveSlideData = () => { 74 | try { 75 | const jsonTalks = extractSlideData('content'); 76 | const dataPath = path.resolve(path.join('data', 'slides.json')); 77 | fs.writeFileSync(dataPath, jsonTalks); 78 | } catch (err) { 79 | console.log(err); 80 | process.exit(1); 81 | } 82 | }; 83 | 84 | saveSlideData(); 85 | 86 | module.exports = { 87 | extractTitleFromMarkdown, 88 | extractSlideData, 89 | listFilesInDirectory, 90 | }; 91 | -------------------------------------------------------------------------------- /first-ng-app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "first-ng-app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist/first-ng-app", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": ["zone.js"], 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | { 28 | "glob": "**/*", 29 | "input": "public" 30 | } 31 | ], 32 | "styles": ["src/styles.scss"], 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 | "outputHashing": "all" 50 | }, 51 | "development": { 52 | "optimization": false, 53 | "extractLicenses": false, 54 | "sourceMap": true 55 | } 56 | }, 57 | "defaultConfiguration": "production" 58 | }, 59 | "serve": { 60 | "builder": "@angular-devkit/build-angular:dev-server", 61 | "configurations": { 62 | "production": { 63 | "buildTarget": "first-ng-app:build:production" 64 | }, 65 | "development": { 66 | "buildTarget": "first-ng-app:build:development" 67 | } 68 | }, 69 | "defaultConfiguration": "development" 70 | }, 71 | "extract-i18n": { 72 | "builder": "@angular-devkit/build-angular:extract-i18n" 73 | }, 74 | "test": { 75 | "builder": "@angular-devkit/build-angular:karma", 76 | "options": { 77 | "polyfills": ["zone.js", "zone.js/testing"], 78 | "tsConfig": "tsconfig.spec.json", 79 | "inlineStyleLanguage": "scss", 80 | "assets": [ 81 | { 82 | "glob": "**/*", 83 | "input": "public" 84 | } 85 | ], 86 | "styles": ["src/styles.scss"], 87 | "scripts": [] 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-in-90ish", 3 | "version": "4.4.0", 4 | "description": "", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "npm run extract && webpack serve", 8 | "build": "npm run extract && cross-env NODE_ENV=production webpack --mode production", 9 | "extract": "node scripts/extractSlideData.js", 10 | "lint": "eslint ./js/**/* --fix", 11 | "serve:prod": "npm run build && npx http-server ./dist -c-1 -o /angular-in-90ish/" 12 | }, 13 | "author": { 14 | "name": "Muhammad Ahsan Ayaz", 15 | "email": "ahsan.ubitian@gmail.com", 16 | "web": "https://codewithahsan.dev" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/ahsanayaz/slides.git" 21 | }, 22 | "engines": { 23 | "node": ">=18.0.0" 24 | }, 25 | "keywords": [ 26 | "reveal", 27 | "slides", 28 | "presentation" 29 | ], 30 | "devDependencies": { 31 | "@babel/core": "^7.24.4", 32 | "@babel/eslint-parser": "^7.14.3", 33 | "@babel/preset-env": "^7.24.4", 34 | "autoprefixer": "^10.4.19", 35 | "babel-loader": "^9.1.3", 36 | "babel-plugin-transform-html-import-to-string": "0.0.1", 37 | "clean-webpack-plugin": "^4.0.0", 38 | "colors": "^1.4.0", 39 | "copy-webpack-plugin": "^12.0.2", 40 | "core-js": "^3.12.1", 41 | "cross-env": "^7.0.3", 42 | "css-loader": "^7.1.1", 43 | "css-minimizer-webpack-plugin": "^6.0.0", 44 | "eslint": "^8.57.0", 45 | "eslint-plugin-react": "^7.34.1", 46 | "eslint-webpack-plugin": "^4.1.0", 47 | "fitty": "^2.3.0", 48 | "gh-pages": "^4.0.0", 49 | "highlight.js": "^11.9.0", 50 | "html-webpack-plugin": "^5.6.0", 51 | "jest": "^29.7.0", 52 | "marked": "^4.0.12", 53 | "mini-css-extract-plugin": "^2.8.1", 54 | "postcss": "^8.4.38", 55 | "postcss-loader": "^8.1.1", 56 | "prettier": "^2.8.0", 57 | "puppeteer": "^22.6.4", 58 | "sass": "^1.75.0", 59 | "sass-loader": "^14.2.0", 60 | "style-loader": "^4.0.0", 61 | "terser-webpack-plugin": "^5.3.10", 62 | "webpack": "^5.91.0", 63 | "webpack-cli": "^5.1.4", 64 | "webpack-dev-server": "^5.0.4" 65 | }, 66 | "browserslist": "> 2%, not dead", 67 | "eslintConfig": { 68 | "env": { 69 | "browser": true, 70 | "es6": true 71 | }, 72 | "parser": "@babel/eslint-parser", 73 | "parserOptions": { 74 | "sourceType": "module", 75 | "allowImportExportEverywhere": true, 76 | "requireConfigFile": false 77 | }, 78 | "globals": { 79 | "module": false, 80 | "console": false, 81 | "unescape": false, 82 | "define": false, 83 | "exports": false 84 | }, 85 | "rules": { 86 | "curly": 0, 87 | "eqeqeq": 2, 88 | "wrap-iife": [ 89 | 2, 90 | "any" 91 | ], 92 | "no-use-before-define": [ 93 | 2, 94 | { 95 | "functions": false 96 | } 97 | ], 98 | "new-cap": 2, 99 | "no-caller": 2, 100 | "dot-notation": 0, 101 | "no-eq-null": 2, 102 | "no-unused-expressions": 0 103 | } 104 | }, 105 | "dependencies": { 106 | "reveal.js": "^5.1.0", 107 | "uuid": "^9.0.1" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | const ESLintPlugin = require('eslint-webpack-plugin'); 9 | const fs = require('fs'); 10 | 11 | const BASE_HREF = 'angular-in-90ish'; 12 | 13 | // Function to generate HtmlWebpackPlugin instances for each HTML file 14 | function generateHtmlPlugins(templateDir) { 15 | const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir)); 16 | return templateFiles 17 | .filter((fileName) => fileName.endsWith('html')) 18 | .map((item) => { 19 | const parts = item.split('.'); 20 | const name = parts[0]; 21 | const extension = parts[1]; 22 | return new HtmlWebpackPlugin({ 23 | filename: `${name}.html`, 24 | template: path.resolve( 25 | __dirname, 26 | `${templateDir}/${name}.${extension}` 27 | ), 28 | base: process.env.NODE_ENV !== 'production' ? '/' : `/${BASE_HREF}/`, 29 | inject: true, 30 | minify: false, 31 | }); 32 | }); 33 | } 34 | 35 | const htmlPlugins = generateHtmlPlugins('./content'); 36 | 37 | module.exports = { 38 | entry: './js/slides.js', 39 | output: { 40 | filename: 'bundle.[contenthash].js', 41 | path: path.resolve( 42 | __dirname, 43 | process.env.NODE_ENV === 'production' ? `dist/${BASE_HREF}` : 'dist' 44 | ), 45 | }, 46 | mode: 'development', // Change to 'production' when ready to deploy 47 | devtool: 'source-map', 48 | plugins: [ 49 | new CleanWebpackPlugin(), 50 | new HtmlWebpackPlugin({ 51 | template: './index.html', 52 | inject: false, 53 | minify: false, 54 | }), 55 | ...htmlPlugins, 56 | new MiniCssExtractPlugin({ 57 | filename: 'styles/[name].[contenthash].css', 58 | }), 59 | new CopyWebpackPlugin({ 60 | patterns: [ 61 | { from: 'content', to: 'content' }, 62 | { from: 'data', to: 'data' }, 63 | { from: 'main.js', to: 'main.js' }, 64 | { from: 'profiles', to: 'profiles' }, 65 | { from: 'assets', to: 'assets' }, 66 | ], 67 | }), 68 | new ESLintPlugin(), 69 | ], 70 | module: { 71 | rules: [ 72 | { 73 | test: /\.js$/, 74 | exclude: /node_modules/, 75 | use: 'babel-loader', 76 | }, 77 | { 78 | test: /\.scss$/, 79 | use: [ 80 | MiniCssExtractPlugin.loader, 81 | 'css-loader', 82 | 'postcss-loader', 83 | 'sass-loader', 84 | ], 85 | }, 86 | { 87 | test: /\.css$/, 88 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 89 | }, 90 | { 91 | test: /\.(png|jpeg|jpg|gif|svg)$/i, 92 | type: 'asset/resource', 93 | }, 94 | ], 95 | }, 96 | optimization: { 97 | minimize: true, 98 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], 99 | }, 100 | devServer: { 101 | static: { 102 | directory: path.join(__dirname, 'dist'), 103 | watch: true, 104 | }, 105 | watchFiles: ['content/**/*', 'css/**/*', 'profiles/*'], 106 | open: true, 107 | port: 8000, 108 | hot: true, 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /assets/images/app-mockup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | HeaderComponentHomeComponentAppComponent -------------------------------------------------------------------------------- /assets/images/components-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | AppComponentHeaderComponentHomeComponent -------------------------------------------------------------------------------- /content/angular-in-90ish.md: -------------------------------------------------------------------------------- 1 | # Angular in 90-ish minutes 2 | 3 | 4 | #### Muhammad Ahsan Ayaz 5 | 6 | --- 7 | 8 | ## What you should know so far? 9 | 10 | - HTML 11 | - JavaScript (or TypeScript) 12 | - CSS (a bit) 13 | - Git 14 | - Basic programming concepts (variables, loops, functions, conditionals) 15 | 16 | --- 17 | 18 | ## Which tools do we need? 19 | 20 | - [VSCode](https://code.visualstudio.com/Download) 21 | - [NodeJS](https://nodejs.org/en/download/prebuilt-installer) 22 | - [Git](https://git-scm.com/downloads) 23 | - [Angular CLI](https://www.npmjs.com/package/@angular/cli) 24 | - [Angular Language Server extension](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template) 25 | 26 | --- 27 | 28 | ## What is Angular 29 | 30 | - Web Applications framework for Single Page Apps (SPA) 31 | - Built by Google 32 | - Has a huge commmunity 33 | 34 | --- 35 | 36 | ![ng-cookbook](assets/images/ng-cookbook-2.png) 37 | 38 | --- 39 | 40 | ## Angular Cookbook 41 | 42 | - Winning Component Communication 43 | - Working with Angular Directives and Built-In Control Flow 44 | - The Magic of Dependency Injection in Angular 45 | - Understanding Angular Animations 46 | - Angular and RxJS – Awesomeness Combined 47 | - Reactive State Management with NgRx 48 | - Understanding Angular Navigation and Routing 49 | 50 | -- 51 | 52 | ## Angular Cookbook 53 | 54 | - Mastering Angular Forms 55 | - Angular and the Angular CDK 56 | - Writing Unit Tests in Angular with Jest 57 | - E2E Tests in Angular with Cypress 58 | - Performance Optimization in Angular 59 | - Building PWAs with Angular 60 | 61 | --- 62 | 63 | ## Benefits of Angular 64 | 65 | - Faster development 66 | - Faster code generation (CLI) 67 | - Unit-tests ready 68 | - Opinionated 69 | - Makes it easy to switch companies and teams 70 | - Code reusability 71 | 72 | --- 73 | 74 | ## Angular vs React 75 | ### Myths about Angular 76 | 77 | -- 78 | 79 | ### Angular vs React 80 |
    81 | 82 | #### Angular 83 | - Is a framework 84 | - Has a built-in CLI 85 | - Has tools and packages included for small-medium scale apps 86 | - Is opinionated (better code style consistency) 87 | 88 | -- 89 | ### Angular vs React 90 |
    91 | 92 | #### React 93 | - Is a library 94 | - Does not have a CLI 95 | - Requires you to install additional packages even for small scale apps 96 | 97 | 98 | -- 99 | ## Myths about Angular 100 | 101 | - It is hard to learn 102 | - Will change significantly on every update 103 | - Angular is slow ([Not really](https://krausest.github.io/js-framework-benchmark/2024/table_chrome_129.0.6668.58.html)) 104 | 105 | 106 | - Angular has a huge bundle size 107 | 108 | --- 109 | 110 | ## Angular Core Concepts 111 | - Components, Services 112 | - Directive, Pipes 113 | - Data-Binding, Event Handlers 114 | - Http Module, Forms Module 115 | - Routing, Animations 116 | - Testing, Building for production 117 | 118 | --- 119 | 120 | ## Creating an Angular app 121 | 122 | ```bash 123 | # install the @angular/cli 124 | npm install -g @angular/cli 125 | 126 | # check cli version 127 | ng --version 128 | 129 | # create an app 130 | ng new first-ng-app # optionally use --dry-run 131 | 132 | # create an app with some configuration 133 | ng new first-ng-app --inline-style --inline-template 134 | ``` 135 | 136 | --- 137 | 138 | ## Angular Components 139 | 140 | Example 141 | 142 | ![components](assets/images/components-1.svg) 143 | 144 | -- 145 | 146 | ![components-markup-1](assets/images/app-mockup.svg) 147 | 148 | -- 149 | 150 | #### Creating a component 151 | ```bash 152 | ng g c header # short form 153 | ng generate component header # full form 154 | # creates inside the `src/app` folder 155 | 156 | 157 | # OR (in a nested directory) 158 | ng g c components/header 159 | # creates HeaderComponent 160 | # inside the `src/app/components` folder 161 | 162 | ng g c home 163 | # creates the HomeComponent 164 | ``` 165 | 166 | -- 167 | 168 | ## Let's style the header and home 169 | 170 | --- 171 | 172 | ## Angular Data-Binding 173 | 174 | Binding data between the TypeScript class of the component, and the component's template. 175 | 176 | -- 177 | 178 | ### Data Binding with Modern Angular (with Signals) 179 | 180 | ```ts 181 | import { Component, signal } from '@angular/core'; 182 | @Component({ 183 | ..., 184 | template: ` 185 |

    Here's my var's value: {{myVar()}}

    186 | ` 187 | }) 188 | class MyComponent { 189 | myVar = signal('some value'); 190 | } 191 | ``` 192 | 193 | more on signals later... 194 | 195 | -- 196 | 197 | ### Data Binding without Signals (traditional way) 198 | 199 | ```ts 200 | import { Component } from '@angular/core'; 201 | @Component({ 202 | ..., 203 | template: ` 204 |

    Here's my var's value: {{myVar}}

    205 | ` 206 | }) 207 | class MyComponent { 208 | myVar = 'some value'; 209 | } 210 | ``` 211 | 212 | -- 213 | 214 | ![home with nested components](assets/images/home-with-nested-components.svg) 215 | 216 | -- 217 | 218 | ### Creating `GreetingComponent` 219 | 220 | ```bash 221 | ng g c components/greeting 222 | # generates in `src/app/components` 223 | ``` 224 | 225 | -- 226 | 227 | #### Passing data from parent to child component via Inputs 228 | 229 | We'll pass the greeting message from the AppComponent 230 | 231 | --- 232 | 233 | ### Event listeners in Angular 234 | 235 | ```html 236 | 237 | 238 | ``` 239 | 240 | ```ts 241 | class MyComponent { 242 | keyUpHandler() { 243 | console.log('user typed something in the input'); 244 | } 245 | } 246 | ``` 247 | 248 | -- 249 | 250 | ### Event listeners in Angular 251 | 252 | ```html 253 | 254 | 255 | ``` 256 | 257 | ```ts 258 | class MyComponent { 259 | keyUpHandler(event: KeyboardEvent) { 260 | console.log(`user pressed the ${event.key} key`); 261 | } 262 | } 263 | ``` 264 | 265 | -- 266 | 267 | ## Let's create a counter component 268 | 269 | ```bash 270 | ng g c components/counter 271 | ``` 272 | 273 | --- 274 | 275 | ## Routing in Angular 276 | 277 | -- 278 | 279 | Angular is a single page application. Using routes, you can still define different pages that the user can navigate to. 280 | 281 | The browser only loads the bundles related to the route user has accessed. 282 | 283 | This significantly improves the performance of the app, and user experience. 284 | 285 | -- 286 | 287 | ## Let's create a new route 288 | 289 | -- 290 | 291 | ![todos-route](assets/images/todos-route.svg) 292 | 293 | -- 294 | 295 | #### Create another component (as a page) for the route 296 | 297 | ```bash 298 | ng g c todos 299 | # this will be the page for todos' list 300 | ``` 301 | 302 | -- 303 | 304 | ![todoItem](assets/images/todo-item.svg) 305 | 306 | -- 307 | 308 | #### Create a component for each todo item 309 | 310 | ```bash 311 | ng g c components/todo-item 312 | ``` 313 | 314 | --- 315 | 316 | ## Angular Services 317 | 318 | -- 319 | 320 | #### Angular Services 321 | 322 | Angular Services are used to encapsulate data, making HTTP calls, or performing any task that is not related directly to data rendering (in my opinion). 323 | 324 | -- 325 | 326 | #### Creating an Angular Service 327 | 328 | ```bash 329 | ng g service services/todos 330 | # creates todos.service.ts inside `src/app/services` 331 | ``` 332 | 333 | -- 334 | 335 | #### Example of serving data from an Angular Service 336 | 337 | -- 338 | 339 | ## Making HTTP calls with Angular Services 340 | 341 | - Provide HTTP module/providers in the app config using `provideHttpClient()` 342 | - Inject the `HttpClient` service 343 | - Use the `http` methods 344 | 345 | --- 346 | 347 | ## Angular Directives 348 | 349 | Angular Directives allow you to add additional behavior to elements in our Angular applications. 350 | 351 | -- 352 | 353 | #### Types of Angular Directives 354 | 355 | - Components 356 | - Attribute directives 357 | - Structural directives 358 | 359 | -- 360 | 361 | #### Let's create an Angular directive for completed todos 362 | 363 | ```bash 364 | ng g directive directives/highlight-completed-todo 365 | ``` 366 | 367 | --- 368 | 369 | ## Angular Pipes 370 | 371 | -- 372 | 373 | Angular pipes are used to transform data right in the templates 374 | 375 | -- 376 | 377 | ## [Built-in Angular pipes](https://angular.dev/guide/templates/pipes) 378 | 379 | -- 380 | 381 | ## Let's create a todos filter pipe 382 | 383 | ```bash 384 | ng g pipe pipes/filter-todos 385 | ``` 386 | 387 | --- 388 | 389 | ## Thank you! 390 | -------------------------------------------------------------------------------- /assets/images/home-with-nested-components.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | AppComponentHeaderComponentHomeComponentCounterComponentGreeterComponent -------------------------------------------------------------------------------- /assets/images/todos-route.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | App ComponentHeaderRouterOutletHomeCounterGreeterTodos -------------------------------------------------------------------------------- /assets/images/todo-item.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | App ComponentHeaderRouterOutletHomeCounterGreeterTodosUserItemUserItemTodoItem --------------------------------------------------------------------------------