From 811d31c2c628cbc9d123bf2798270b47b494c4e6 Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:03:57 +0530 Subject: [PATCH 1/8] Remove AngularJs references with Angular as migration is done --- Learning-Resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Learning-Resources.md b/Learning-Resources.md index bd657dd8..c94b7321 100644 --- a/Learning-Resources.md +++ b/Learning-Resources.md @@ -1,4 +1,4 @@ -In general, it's easier to contribute to the Oppia codebase if you have some knowledge of git, as well as at least one of Python or AngularJS/Angular. You don't need to know all of this before you start, though! Many of our contributors have picked these skills up concurrently while tackling their first issues. +In general, it's easier to contribute to the Oppia codebase if you have some knowledge of git, as well as at least one of Python or Angular. You don't need to know all of this before you start, though! Many of our contributors have picked these skills up concurrently while tackling their first issues. That said, we strongly recommend that you be open to learning new things. If you need to brush up on some of the technologies used in Oppia, here are some resources that may help: From f6f6f4736ddeae93552f35f7cb76a79ff9008851 Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:34:24 +0530 Subject: [PATCH 2/8] Update Frontend-tests.md --- Frontend-tests.md | 150 ++++++++-------------------------------------- 1 file changed, 25 insertions(+), 125 deletions(-) diff --git a/Frontend-tests.md b/Frontend-tests.md index dd096ae0..a45aa43f 100644 --- a/Frontend-tests.md +++ b/Frontend-tests.md @@ -43,21 +43,16 @@ * [Handling asynchronous code](#handling-asynchronous-code) * [Making HTTP calls](#making-http-calls) * [Setting up CsrfToken](#setting-up-csrftoken) - * [HTTP calls in AngularJS](#http-calls-in-angularjs) - * [HTTP calls in Angular 2+](#http-calls-in-angular-2) + * [HTTP calls in Angular](#http-calls-in-angular) * [Using `done` and `done.fail` from Jasmine](#using-done-and-donefail-from-jasmine) * [Handling `$timeout` correctly](#handling-timeout-correctly) - * [Mocking with `$q` API in AngularJS](#mocking-with-q-api-in-angularjs) * [When upgraded services should be imported in the test file](#when-upgraded-services-should-be-imported-in-the-test-file) - * [`beforeEach` calls in AngularJS](#beforeeach-calls-in-angularjs) * [How to handle common errors](#how-to-handle-common-errors) * [Testing services](#testing-services) - * [Testing AngularJS services](#testing-angularjs-services) - * [Testing Angular 2+ services](#testing-angular-2-services) + * [Testing Angular services](#testing-angular-services) * [Testing controllers](#testing-controllers) * [Testing directives and components](#testing-directives-and-components) - * [Testing AngularJS directives and components](#testing-angularjs-directives-and-components) - * [Testing Angular2+ directives and components](#testing-angular2-directives-and-components) + * [Testing Angular directives and components](#testing-angular-directives-and-components) * [Contacts](#contacts) ## Introduction @@ -103,8 +98,7 @@ When we achieve our goal, then for every frontend code file, executing only its This list contains some resources that might help you while writing unit tests: - [Jasmine documentation](https://jasmine.github.io/api/edge/global) - [Karma](https://karma-runner.github.io/) -- [Angular 2+ testing](https://angular.io/guide/testing) -- [AngularJS testing](https://docs.angularjs.org/guide/unit-testing) +- [Angular testing](https://angular.io/guide/testing) ## Run frontend tests @@ -193,17 +187,7 @@ The `beforeEach` function is used to set up essential configurations and variabl * Injecting the modules to be tested or to be used as helpers inside the test file. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/splash-page/splash-page.controller.spec.ts#L37-L49) is an example. -* Mocking the unit test’s external dependencies (only on AngularJS files): - - ```js - beforeEach(angular.mock.module(function($provide) { - $provide.value('ExplorationStatesService', { - getState: () => ({ interaction: null }) - }); - })); - ``` - -* Providing Angular2+ services in downgrade files when the AngularJS service being tested uses any upgraded (Angular2+) service as a dependency. For example, assume that the test requires MyExampleService which is an Angular2+ service: +* Providing Angular services in test files when the code being tested depends on them. For example, assume that the test requires MyExampleService: ```js import { TestBed } from '@angular/core/testing'; @@ -214,10 +198,7 @@ The `beforeEach` function is used to set up essential configurations and variabl imports: [HttpClientTestingModule] }); }); - beforeEach(angular.mock.module('oppia', function($provide) { - $provide.value('MyExampleService', - TestBed.get(MyExampleService)); - })); + ``` #### `it` @@ -230,7 +211,7 @@ Like `describe`, the `it` function has the variants `fit` and `xit` and they can #### `afterEach` -The `afterEach` function runs after each test, and it is not used often. It’s mostly used when we are handling async features such as HTTP and timeout calls (both in AngularJS and Angular 2+). [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/domain/exploration/read-only-exploration-backend-api.service.spec.ts#L100-L103)'s an example to handle HTTP mocks in AngularJS and [here](https://github.com/oppia/oppia/blob/ae649aa08f/core/templates/domain/classroom/classroom-backend-api.service.spec.ts#L72-L74)'s an example of doing the same in Angular 2+. +The `afterEach` function runs after each test, and it is not used often. It’s mostly used when we are handling async features such as HTTP and timeout calls. [Here](https://github.com/oppia/oppia/blob/ae649aa08f/core/templates/domain/classroom/classroom-backend-api.service.spec.ts#L72-L74) is an example. #### `afterAll` @@ -413,7 +394,7 @@ One of the main features of Jasmine is allowing you to spy on a method or proper * You can mock a property value or a method return. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data.service.spec.ts#L147)'s an example. -* You can provide fake implementations that will be called when a method is executed. This is commonly used when mocking AngularJS promises with `$defer`. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/email-dashboard-pages/email-dashboard-page.controller.spec.ts#L121-L133)'s an example. +* You can provide fake implementations that will be called when a method is executed. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/email-dashboard-pages/email-dashboard-page.controller.spec.ts#L121-L133)'s an example. * You can spy on a method to check whether that method is being called when the spec runs. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/services/schema-default-value.service.spec.ts#L109-L118)'s an example. @@ -476,32 +457,19 @@ All HTTP calls must be mocked since the frontend tests actually run without a ba ##### Setting up CsrfToken -In order to make HTTP calls in a secure way, it's common that applications have tokens to authenticate the user while they are using the platform. In the codebase, there is a specific service to handle the token, called CsrfTokenService. When mocking HTTP calls, you must mock this service in the test file so the tests won't fail due to lacking a token. Then, you should just copy and paste this piece of code inside a `beforeEach` block (the CsrfService will be a variable with the return of `$injector.get('CsrfTokenService')` -- in AngularJS -- or `TestBed.get(CsrfTokenService)` -- in Angular 2+): +In order to make HTTP calls in a secure way, it's common that applications have tokens to authenticate the user while they are using the platform. In the codebase, there is a specific service to handle the token, called CsrfTokenService. When mocking HTTP calls, you must mock this service in the test file so the tests won't fail due to lacking a token. Then, you should just copy and paste this piece of code inside a `beforeEach` block (the CsrfService will be a variable with the return of `$injector.get('CsrfTokenService')`): ```js -spyOn(CsrfService, 'getTokenAsync').and.callFake(function() { - var deferred = $q.defer(); - deferred.resolve('sample-csrf-token'); - return deferred.promise; -}); +spyOn(CsrfService, 'getTokenAsync').and.returnValue( + Promise.resolve('sample-csrf-token') +); ``` -##### HTTP calls in AngularJS - -[Here](https://github.com/oppia/oppia/blob/ae649aa08f1375457ec9e3c90257197b68fec7cd/core/templates/domain/learner_dashboard/learner-playlist.service.spec.ts#L84-L99) is an example which uses `$httpBackend` to mock the backend responses. - -To mock a backend call, you need to use the `$httpBackend` dependency. There are two ways to expect an HTTP method (you can use both): - -* `$httpBackend.expectMETHODNAME(URL)` - like `expectPOST` or `expectGET` for instance -* `$httpBackend.expect(‘METHOD’, URL)` - You pass the HTTP method as the first argument. +##### HTTP calls in Angular -When writing HTTP tests (which are asynchronous) we need to always use the `$httpBackend.flush()` method. This will ensure that the mocked request call will be executed. +When writing HTTP tests on Angular, use `httpTestingController` with `fakeAsync()` and `flushMicrotasks()`. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/domain/classroom/classroom-backend-api.service.spec.ts#L77-L96)’s a good example to follow. -##### HTTP calls in Angular 2+ - -When writing HTTP tests on Angular 2+, use `httpTestingController` with `fakeAsync()` and `flushMicrotasks()`. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/domain/classroom/classroom-backend-api.service.spec.ts#L77-L96)’s a good example to follow. - -Just like the AngularJS way to mock HTTP calls, the Angular 2+ has flush functions to return the expected response and execute the mock correctly. +Angular provides methods to flush mocked requests, return the expected response, and execute the mock correctly. #### Using `done` and `done.fail` from Jasmine @@ -511,38 +479,9 @@ There’s a specific case where you should use `done` on mocking HTTP calls: whe You can use `done` when using `setTimeout` for specific cases as well, check out this [example](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/teach-page/teach-page.controller.spec.ts#L53-L67). -#### Handling `$timeout` correctly - -We use `$timeout` a lot across the codebase. When testing a `$timeout` callback, we used to call another `$timeout` in the unit tests, in order to wait for the original callback to be called. However, this approach was tricky and it was making the tests fail. When testing `$timeout` behavior, you should use [$flushPendingTasks](https://docs.angularjs.org/api/ngMock/service/$flushPendingTasks), which is cleaner and less error-prone than `$timeout`. Here's an example: - -**Bad code:** - -```js -it('should wait for 10 seconds to call console.log', function() { - spyOn(console, 'log'); - $timeout(function() { - expect(console.log).toHaveBeenCalled(); - }, 10); -}); -``` - -**Good code:** - -```js -it('should wait for 10 seconds to call console.log', function() { - spyOn(console, 'log'); - $flushPendingTasks(); - expect(console.log).toHaveBeenCalled(); -}); -``` - -#### Mocking with `$q` API in AngularJS - -When mocking a promise in AngularJS, you might use the `$q` API. In these cases, you must use `$scope.$apply()` or `$scope.$digest` because they force `$q` promises to be resolved through a Javascript digest. Here are some examples using [$apply](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/email-dashboard-pages/email-dashboard-page.controller.spec.ts#L101-L108) and [$digest](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/exploration-editor-page/services/exploration-states.service.spec.ts#L209-L221). - ### When upgraded services should be imported in the test file -One of the active projects in Oppia is the Angular2+ migration. When testing AngularJS files which rely on an Angular2+ dependency, you must use a `beforeEach` call below to import the service. For example, assume that the test requires MyExampleService which is an Angular2+ service. +When testing code that depends on Angular services, you may need to import and provide those services in the test setup. For example, assume that the test requires MyExampleService. ```js import { TestBed } from '@angular/core/testing'; @@ -553,25 +492,16 @@ import { MyExampleService } from 'services/my-example.service'; imports: [HttpClientTestingModule] }); }); -beforeEach(angular.mock.module('oppia', function($provide) { - $provide.value('MyExampleService', - TestBed.get(MyExampleService)); -})); -``` - -If the file you’re testing doesn’t use any upgraded files, you don’t need to use this `beforeEach` call. - -### `beforeEach` calls in AngularJS -If you’re testing an AngularJS file that uses an upgraded service, you’ll need to include a `beforeEach` block which mocks all the upgraded services. [Here](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.controller.spec.ts#L35-L48) is an example. +``` -However, you might face the following situation: you need to mock an Angular2+ service by using `$provide.value`. Here’s the problem: if you use `$provide.value` before calling the updated services, your mock will be overwritten by the original code of the service. So, you need to change the order of `beforeEach` calls, as you can see in [this test](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/exploration-editor-page/improvements-tab/services/improvement-suggestion-thread-modal.controller.spec.ts#L36-L51). +If the file you’re testing does not depend on additional Angular services, you may not need this setup. ### How to handle common errors * If you see an error like `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module`, it means you are using a service (directly or indirectly) that is upgraded to Angular. - * Your test that is written in AngularJS is unable to get that particular service. You can fix this by providing the value of the Angular2+ service using $provide. For example, let us assume that the test requires MyExampleService which is an Angular2+ service. Then you can provide the service like this: + * This usually means the required Angular service has not been provided in the test setup. You can fix this by configuring the service in TestBed before running the test. ```js import { TestBed } from '@angular/core/testing'; @@ -582,14 +512,9 @@ However, you might face the following situation: you need to mock an Angular2+ s imports: [HttpClientTestingModule] }); }); - beforeEach(angular.mock.module('oppia', function($provide) { - $provide.value('MyExampleService', - TestBed.get(MyExampleService)); - })); + ``` -* If you’re working with async on AngularJS and your tests don’t seem to run correctly, make sure you’re using `$apply` or `$digest` in the spec, as in this [example](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/topic-editor-page/services/topic-editor-state.service.spec.ts#L716-L720). - ## Testing services Services are one of the most important features in the codebase. They contain logic that can be used across the codebase multiple times. There are three possible extensions for services: @@ -601,15 +526,7 @@ Services are one of the most important features in the codebase. They contain lo As a good first issue, all the services that need to be tested are listed in [issue #4057](https://github.com/oppia/oppia/issues/4057). -### Testing AngularJS services - -Use these files that are correctly following the testing patterns for reference: - -* [current-interaction.service.spec.ts](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/pages/exploration-player-page/services/current-interaction.service.spec.ts#L39) -* [editable-exploration-backend-api.service.spec.ts](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/domain/exploration/editable-exploration-backend-api.service.spec.ts#L30) -* [improvement-task.service.spec.ts](https://github.com/oppia/oppia/blob/2e60d69d7b/core/templates/services/improvement-task.service.spec.ts#L29) - -### Testing Angular 2+ services +### Testing Angular services Use these files that are correctly following the testing patterns for reference: @@ -619,7 +536,7 @@ Use these files that are correctly following the testing patterns for reference: ## Testing controllers -Controllers are used often for AngularJS UI Bootstrap library's modals. Here are some files that are correctly being tested and follow the testing patterns for reference: +Controllers are used often for UI Bootstrap library's modals. Here are some files that are correctly being tested and follow the testing patterns for reference: * [welcome-modal.controller.spec.ts](https://github.com/oppia/oppia/blob/aa288fd246dec2f8a30a1e9f72a77bd97952c132/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.controller.spec.ts) * [merge-skill-modal.controller.spec.ts](https://github.com/oppia/oppia/blob/3642a4c21e387493f85c7bb72fe1789d214ffffb/core/templates/components/skill-selector/merge-skill-modal.controller.spec.ts) @@ -635,26 +552,9 @@ Also, there are controllers that are not linked to modals. Here is an example: ## Testing directives and components -### Testing AngularJS directives and components - -> [!NOTE] -> If you're creating a new AngularJS directive, please make sure the value of the restrict `property` is not `E`. If it's an `E`, change the directive to an AngularJS component. You can check out [this PR](https://github.com/oppia/oppia/pull/9850) to learn how to properly make the changes. - -Use these AngularJS component files that are correctly following the testing patterns for reference: - -* [search-bar.component.spec.ts](https://github.com/oppia/oppia/blob/a9bece78fd45344f5e0e741ab21f8ea0c289a923/core/templates/pages/library-page/search-bar/search-bar.component.spec.ts) -* [preferences-page.component.spec.ts](https://github.com/oppia/oppia/blob/3642a4c21e387493f85c7bb72fe1789d214ffffb/core/templates/pages/preferences-page/preferences-page.component.spec.ts) -* [practice-tab.component.spec.ts](https://github.com/oppia/oppia/blob/fcb44f8cc6e0e00aaa082045cf8b363daa510479/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.spec.ts) - -Use these AngularJS directive files that are correctly following the testing patterns for reference: - -* [value-generator-editor.directive.spec.ts](https://github.com/oppia/oppia/blob/7aa80c49f81270c886818e3dce587715dcebac68/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.spec.ts) -* [audio-translation-bar.directive.spec.ts](https://github.com/oppia/oppia/blob/4ec7b9cc70e2a255653952450fe44932607755af/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.directive.spec.ts) -* [oppia-visualization-click-hexbins.directive.spec.ts](https://github.com/oppia/oppia/blob/89a809b521af0c2d21b71db5cdc8c644d893a577/extensions/visualizations/oppia-visualization-click-hexbins.directive.spec.ts) - -### Testing Angular2+ directives and components +### Testing Angular directives and components -Let us assume that we are writing tests for an Angular2+ component called BannerComponent. The first thing to do is to import all dependencies, we have a boilerplate for that: +Let us assume that we are writing tests for an Angular component called BannerComponent. The first thing to do is to import all dependencies, we have a boilerplate for that: ```js import { async, ComponentFixture, TestBed } from '@angular/core/testing'; @@ -687,7 +587,7 @@ Once this is done, you have the class instance in the variable called `component At the moment, we don't enforce [DOM testing](https://angular.io/guide/testing-components-basics#component-dom-testing). However, as the docs say, the component is not fully tested until we test the DOM too. Eventually we hope to add DOM tests for all our components, however, for now if you are making a PR fixing a bug caused due to incorrect DOM bindings, then add DOM tests for that component. Our coverage checks do not require DOM tests. -Use these Angular2+ component files that are correctly following the testing patterns for reference: +Use these Angular component files that are correctly following the testing patterns for reference: * [donate-page.component.spec.ts](https://github.com/oppia/oppia/blob/327df0c22ec839d4ad4232492749c78443b15fb0/core/templates/pages/donate-page/donate-page.component.spec.ts) * [teach-page.component.spec.ts](https://github.com/oppia/oppia/blob/13b1da20ee6c0e4eabc9720a3d1ca3d87c62fe8c/core/templates/pages/teach-page/teach-page.component.spec.ts) From 978fa7eb91bee7910906f85dcde62274c28b83df Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:50:03 +0530 Subject: [PATCH 3/8] Update terminology from AngularJS to Angular --- Overview-of-the-Oppia-codebase.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Overview-of-the-Oppia-codebase.md b/Overview-of-the-Oppia-codebase.md index 8944a73e..c6603e32 100644 --- a/Overview-of-the-Oppia-codebase.md +++ b/Overview-of-the-Oppia-codebase.md @@ -16,7 +16,7 @@ * [Extensions](#extensions) * [Other files and folders](#other-files-and-folders) -Oppia is built with [Google App Engine](https://developers.google.com/appengine/docs/whatisgoogleappengine). Its backend is written in [Python](https://www.python.org/), and its frontend is written using [AngularJS](https://angularjs.org/) and [Angular](https://angular.io). +Oppia is built with [Google App Engine](https://developers.google.com/appengine/docs/whatisgoogleappengine). Its backend is written in [Python](https://www.python.org/), and its frontend is written using [Angular](https://angular.io). ## Web server anatomy: Explaining "frontend" and "backend" @@ -109,7 +109,7 @@ The domain layer (or "business logic" layer) defines both the domain objects and ### Storage layer -Finally, we have the storage layer, which defines the storage models. A storage model is a class that stores the information that defines a particular object in Oppia. For example, we have a model for each exploration. (Note that we use "model" to refer to both the class that defines the exploration model and each instance of that class. Sorry, we know it's confusing, but this language is all over the code base.) +Finally, we have the storage layer, which defines the storage models. A storage model is a class that stores the information that defines a particular object in Oppia. For example, we have a model for each exploration. (Note that we use "model" to refer to both the class that defines the exploration model and each instance of that class. Sorry, we know it's confusing, but this language is all over the codebase.) These are also classes, but they define how data is stored in whatever system we are using to store data to the file system. In production, we use the [Google Cloud Datastore](https://cloud.google.com/datastore), and we interface with it from Python using [Cloud NDB](https://googleapis.dev/python/python-ndb/latest/index.html). @@ -123,18 +123,18 @@ The backend codebase is heavily tested. Tests are contained in `*_test.py` files ## Frontend -Oppia's frontend code is currently being migrated from AngularJS to Angular, so when reading the following sections, you'll see things that have one name in AngularJS and another name in Angular. +Oppia's frontend code is written in Angular, so the follwing sections use Angular terminology throughout. The frontend includes the HTML and CSS code that define what the user sees, and it includes the JavaScript (and TypeScript) code that runs in the user's browser. Oppia's frontend follows the [Model-View-Controller (MVC)](https://developer.mozilla.org/en-US/docs/Glossary/MVC) software design pattern. You should be familiar with that pattern before reading further. -Here are the Angular and AngularJS features we use to implement the MVC paradigm: +Here are the Angular features we use to implement the MVC paradigm: -| MVC Term | Angular Term | AngularJS Term | -|---------------------|--------------|----------------| -| Model | Model | Object Factory | -| View and Controller | Component | Directive | +| MVC Term | Angular Term | +|---------------------|--------------| +| Model | Model | +| View and Controller | Component | Let's start with a diagram of Oppia's frontend architecture. Then we will discuss each layer in turn. @@ -158,7 +158,7 @@ sequenceDiagram ### Component layer -Components (directives in AngularJS) define both the view (the layout of the page the user sees) and the controller logic that responds to user input. +Components define both the view (the layout of the page the user sees) and the controller logic that responds to user input. Most pages begin with a `*.mainpage.html` file, for example `topic-editor-page.mainpage.html`. This HTML file contains a `` tag, which refers to the `topic-editor-page.component.html` file. @@ -195,8 +195,6 @@ export class ProfilePageComponent { } ``` -(Note that here we used the profile page as an example since the topic editor page hadn't been migrated to Angular yet at time of writing.) - Then this code gets used in the component HTML file like this: ```html @@ -228,7 +226,7 @@ Ideally, all interaction with the backend would happen through these backend API Everything we've described so far lives in the "view" and "controller" realms of MVC. Now let's get to the "model" part. -Models (or object factories in AngularJS) are data structures that represent objects in Oppia. For example, we have a model for a user and another for a user's profile. These are just classes that hold information about the object they represent and provide methods for getting that information. They are also known as "frontend domain objects". +Models are data structures that represent objects in Oppia. For example, we have a model for a user and another for a user's profile. These are just classes that hold information about the object they represent and provide methods for getting that information. They are also known as "frontend domain objects". Here's an (overly simplified) example of a model: From a2eaa46b2a7ee9ac650440dfeac0d991ad58739c Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:27:53 +0530 Subject: [PATCH 4/8] Delete Angular-Migration.md Migration is totally complete --- Angular-Migration.md | 1059 ------------------------------------------ 1 file changed, 1059 deletions(-) delete mode 100644 Angular-Migration.md diff --git a/Angular-Migration.md b/Angular-Migration.md deleted file mode 100644 index 068dc7c9..00000000 --- a/Angular-Migration.md +++ /dev/null @@ -1,1059 +0,0 @@ -## Overview - -Angular is an app-design framework and development platform for creating efficient and sophisticated apps. - -Currently, Oppia is in a hybrid state where we have both Angular and AngularJS. This makes our application slow and bulky. The codebase has duplicate libraries since many of the AngularJS libraries are not compatible with Angular. This project aims to migrate the entire codebase to Angular. The benefits of doing this are: - -* Improved Developer Experience: - - * Developing when the application is a hybrid state opens us to a whole host of complicated errors which are in some cases not solvable. - * Angular is being actively maintained and comes out with a lot of new features that aid development. - -* Improved User Experience: - - * When the codebase is completely migrated, the developers will focus their efforts on making new features for the website rather than fixing nasty errors that pop up because of the hybrid state. - * Decreased page loading times as a result of not bundling AngularJS anymore. - * Better application performance in general. - -The project plan will be iterative in nature. We will migrate the services first and then the controllers and directives. The services will be migrated in dependency order. For example, if A depends on B and B depends on C, we will migrate in the order C, B, and then A. - -### Testing videos - -**Note: Angular Migration Pull Requests must be accompanied with a video showing the before and after effects of their change to ensure that nothing is broken. This ensures faster review and a lower risk of reverted PRs** - -## Angular migration tracker - -The [angular migration tracker](https://docs.google.com/spreadsheets/d/1L9Udn-XT6Lk1qaTBUySTw1AnhvQMR-30Qry4rfd-Ovg/edit?usp=sharing) holds the record of which services are to be migrated. The issue [#8472](https://github.com/oppia/oppia/issues/8472) holds a subset of those services that can be migrated without any major blockers. - -## Implementation details to migrate services - -1. Import the following dependencies: - - ```js - import { downgradeInjectable } from '@angular/upgrade/static'; - import { Injectable } from '@angular/core'; - ``` - -2. If the services uses `$http`, import `HttpClient` as a dependency, also import: - - ```js - import { HttpClient } from '@angular/common/http'; - ``` - -3. Change the AngularJS factory definition to and Angular class definition as follows: - - ```js - angular.module('oppia').factory('ServiceName',['dependency1', function(dependency1) { - ``` - - to - - ```js - import { dependency1 } from ... // to be added at the top of the file - .. - .. - export class ServiceName { - ``` - -4. Add a decorator above the class definition: - - ```js - @Injectable({ - providedIn: 'root' - }) - export class ServiceName { - ... - ``` - -5. Add a constructor for the class and inject the dependencies: - - ```js - constructor( - private service1: Service1, - private service2: Service2) {} - ``` - -6. Change `$http.get` requests in the service as follows: - - (a) Change `$http.get` to `this.http.get`: - - ```js - $http.get(url).then(function(response) { - dataDict = angular.copy(response.data); - ``` - - to - - ```js - this.http.get( - url).toPromise().then( - (response) => { - ``` - - The `dataDict` is not required in Angular services. You can directly use the `response` variable. - - (b) Search in the codebase for where the service is used to obtain results from get requests and change `response.data` to `response`. - - (c) Return the `errorCallback` (the reject function) with `errorResponse.error.error` as follows: - - ```js - (errorResponse) => { - errorCallback(errorResponse.error.error); - } - ``` - - (d) Add `$rootScope.$applyAsync()` in the controller/directive that is resolving the HTTP request similar to how it is added [here](https://github.com/oppia/oppia/pull/8427/files#diff-ecf6cefd0707bcbafeb6a0b4009aa60cR78). To do so, perform a global search in the codebase for the function with the HTTP request. For example, if the service is `SkillBackendApiService` and the function in which the HTTP request is made is `fetchSkill`, then search the codebase for `SkillBackendApiService.fetchSkill` and add `$rootScope.$applyAsync()` as follows: - - ```js - SkillBackendApiService.fetchSkill(...).then((...) => { - //resolve function - ... - $rootScope.$applyAsync() //add here - }, (...) => { - ... - //reject function - } - ); - ``` - - Do this for all functions that have `http` calls. - -7. Change `$http.put` or `$http.post` requests as follows: - - (a) Change `$http.post/put` to `this.http.post/put` - - ```js - $http.post(url).then(function(response) { - ... - ``` - - to - - ```js - this.http.post( - url).toPromise().then( - (response) => {...; - ``` - - (b) Add `$rootScope.$applyAsync()` wherever the function with the HTTP request is used. For example, see the changes [here](https://github.com/oppia/oppia/pull/8427/files#diff-ecf6cefd0707bcbafeb6a0b4009aa60cR78). You can find usages of the function just like you found usages when migrating `$http.get` calls in the previous step. - - -8. If you are migrating a service that is named as `.*-backend-api.service.ts`, then please return a domain object and not a dict in the `successCallback` function. For example take a look at [PR #9505](https://github.com/oppia/oppia/pull/9505/files#diff-05de50229b44c01bdaeac172928b514dR64), where the domain object is created via an object factory. You also need to change the piece of code where this response is used because the response is now a domain object instead of a dict. If there is no specific object factory to alter the response to a domain object, create one similar to how it is done in this [change](https://github.com/oppia/oppia/pull/9570/files#diff-09e3c3999c18dabdf2ddedf6e3e250f8R1). - - Topic domain objects need to contain properties that are being read from the backend. Therefore, the topic domain object does not depend on the service being migrated, but rather the expected return value of the function. For example, in `SkillBackendApiService`, the function `fetchSkill` will clearly return a `Skill` object. Note that `SkillObjectFactory.ts` already exists, so we don't need to create it. But if there is no corresponding Object Factory, you need to create one similar to how `SkillObjectFactory` is created. Next, we take the response from the backend and instead of `successCallback(response)`, we resolve `successCallback(SkillObjectFactory.createFromBackendDict(response))`. This passes the frontend `Skill` object to functions that call `fetchSkill` when the promise gets resolved. - - Since before you migrated the file, the calling functions were expecting a backend dict object, the references need to be changed as well. To do this, do a global search in the code-base for the function, e.g. `SkillBackendApiService.fetchSkill` and refactor the code inside the resolve function to reflect that the parameter is now a `Skill` object and not a backend dict object. - - Please note that interfaces/properties in Object Factories and the `.*-backend-api.service.ts` could be in snake_case. If that is the case, please surround them with single quotes as in `'some_property'`. Except for these two categories, all the properties inside all other files should be camel case, e.g. `someProperty`. - -9. For functions in the service, add type definitions for all the arguments as well as return values. - - **Note:** For complex types or some type that is being used over functions or files we can declare an interface. For example in the file [rating-computation.service.ts](https://github.com/oppia/oppia/blob/develop/core/templates/components/ratings/rating-computation/rating-computation.service.ts) we have an export interface to declare the type `RatingFrequencies`. In the same file, we also have a function named static, which is used by the functions of the class itself. - -10. For functions which are private to the service (used as helper functions), add the private keyword. - -11. At the end of the file, add: - - ```js - angular.module('oppia').factory('ServiceName', downgradeInjectable(ServiceName)); - ``` - -For an example of migrating a service, see [this pull request](https://github.com/oppia/oppia/pull/10693/files). - -## Implementation details to migrate tests - -1. Remove all `beforeEach()` blocks and any other service that is not needed in the test file. - -2. Convert all the function keywords to fat arrow functions like this: - - ```js - describe('abc', function() { ... }); - ``` - - to - - ```js - describe('abc', () => { .. }); - ``` - -3. Import TestBed in your spec file - - ```js - import { TestBed } from '@angular/core/testing'; - - import { ServiceName } from ... - ``` - - If your test is for a service that makes HTTP requests, you also need to import the following: - - ```js - import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; - import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - ``` - -4. Add a beforeEach block that creates an instance of service you want to test: - - ```js - beforeEach(() => { - serviceInstance = TestBed.get(ServiceName); - }); - ``` - - (a) If your spec file needs any pipes (filters in angular), import them and add it to the providers in the TestBed configuration - - ```js - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [CamelCaseToHyphensPipe, ConvertToPlainTextPipe] // Any pipe that is required - }); - instance = TestBed.get(ServiceName); - ``` - - (b) If your spec file tests a service that makes HTTP requests, you need to make an `HttpClientTestingModule` and add an `afterEach` statement to check there are no pending requests after each test. For example: - - ```js - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - }); - httpTestingController = TestBed.get(HttpTestingController); - instance = TestBed.get(ServiceName); - }); - - afterEach(() => { - httpTestingController.verify(); - }); - ``` - -5. For each test, replace the name of the service with the instance name that is created above using TestBed. - -6. If your spec file is for a service that makes HTTP requests then: - - (a) Convert each individual test defined in it block as follows: - - ```js - it('should ...', fakeAsync(() => { - . - . - })); - ``` - - (b) Change the test to create an HTTP request via httpTestingController. Here's an example from `topic-viewer-backend-api.service.spec.ts`: - - ```js - $httpBackend.expect('GET', '/topic_data_handler/0').respond( - sampleDataResults); - TopicViewerBackendApiService.fetchTopicData('0').then( - successHandler, failHandler); - $httpBackend.flush(); - ``` - - to - - ```js - topicViewerBackendApiService.fetchTopicData('0').then( - successHandler, failHandler); - var req = httpTestingController.expectOne( - '/topic_data_handler/0'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleDataResults); - - flushMicrotasks(); - ``` - -## Implementation details to migrate directives - -There are two parts to this migration: the TS file and the HTML file. - -### Migrating the logic part (ts file) - -Here are the steps to migrate the logic part. - -#### 1. Create a basic component in the directive file - -Import `Component` to the file: - -```js -import { Component } from '@angular/core'; -``` - -Then take a look at the directive declaration. For example if the directive is declared like this: - -```js -angular.module('oppia').directive('conceptCard', [ - 'UrlInterpolationService', function(UrlInterpolationService) { - return { - restrict: 'E', - scope: {}, - bindToController: { - getSkillIds: '&skillIds', - index: '=' - }, - templateUrl: UrlInterpolationService.getDirectiveTemplateUrl( - '/components/concept-card/concept-card.directive.html'), - controllerAs: '$ctrl', - controller: [ - '$scope', '$filter', '$rootScope', - 'ConceptCardBackendApiService', 'ConceptCardObjectFactory', -``` - -then add the following at the end of the file: - -```js -@Component({ - selector: 'concept-card', - templateUrl: './concept-card.directive.html', - styleUrls: [] -}) -export class ConceptCardComponent {} -``` - -Some points to note: - -* Please keep in mind the name of the directive declared. In this case, it is 'conceptCard'. -* The directive name converted to kebab-case is the selector (conceptCard -> concept-card). -* The name of the class is in CamelCase (note the first letter is capital) suffixed with "Component". So conceptCard -> ConceptCardComponent. -* The template of the directive (in 99% of cases) exists in the same folder as the directive.ts file. - -#### 2. Import and inject the dependencies - -Suppose the AngularJS code has the following dependencies: - -```js -controller: [ - '$scope', '$rootScope', - 'ConceptCardBackendApiService', 'ConceptCardObjectFactory', -``` - -The first two dependencies are interesting ones. They don't have a direct equivalent in Angular. In most cases, you will find something like `$scope.someVariable = `, `$scope.$onDestroy`, `$scope.$onInit`, and `$rootScope.$applyAsync()`. In other cases, contact @srijanreddy98. - -Next, consider the other two dependencies ('ConceptCardBackendApiService', 'ConceptCardObjectFactory'). These are called injectables as they are "injected". These go into your constructor like this: - -1. Import these two services into the directive. - - ```js - import { ConceptCardBackendApiService } from 'service/some-service.ts'; - import { ConceptCardObjectFactory } from 'services/some-other-service.ts'; - ``` - -2. Create a constructor for the class you made in the previous step and add those injectables there: - - ```js - export class ConceptCardComponent { - - constructor( - private conceptCardBackendApiService: ConceptCardBackendApiService, - private conceptCardObjectFactory: ConceptCardObjectFactory - ) {} - - } - ``` - -Sometimes, you will also see dependencies like $window, $log, $timeout etc. - -* For `$window`: Use `window-ref.service.ts`. In the constructor, with other dependencies, inject `WindowRef`: - - ```js - constructor( - ... - private windowRef: WindowRef - ) {} - ``` - - Then instead of `window`, use `windowRef.nativeWindow`, e.g. `windowRef.nativeWindow.location.href`. Also, instead of `location` for setting URLs, use `location.href`. - -* `For $timeout`: Use `setTimeout` - -* `For $log`: Use `logger.service.ts` - -**Notice the casing very carefully. The service is ConceptCardBackendApiService but its instance is called conceptCardBackendApiService (with a small c)** - -#### 3. Adding OnInit / OnDestroy - -You will notice that in almost every directive you have `ctrl.$onInit` and/or `ctrl.$onDestroy`. The equivalent of this in angular is `ngOnInit` and `ngOnDestroy`. - -First, you need to import OnInit from '@angular/core'; Then implement it in the class by changing the class declaration to `export class ConceptCard implements OnInit {` and adding an `ngOnInit() {}` below the constructor. After this step the component class would look like this: - -```js -import { Component, OnInit } from '@angular/core'; -... -export class ConceptCardComponent implements OnInit { - - constructor( - private conceptCardBackendApiService: ConceptCardBackendApiService, - private conceptCardObjectFactory: ConceptCardObjectFactory - ) {} - - ngOnInit(): void { - ... - } -} -``` - -Do the same for `onDestroy`. - -#### 4. Changing the bindToController - -If there is some value in bindToController, then import `Input` from `@angular/core`; - -There are four "syntaxes" that you could run into when trying to migrate bindToController: - -1. `'@'` -2. `'<'` -3. `'='` -4. `'&'` - -##### The syntax for `'@'` - -```js - layoutType: '@', -``` - -will change to - -```js - @Input() layoutType: string; -``` - -**Note that `'@'` is always a string but `'<'` can be of any type (string, number, object or custom types).** - -##### The syntax for '<' - -```js - layoutAlignType: '<', -``` - -changes to - -```js - @Input() layoutAlignType: string; -``` - -(The type of layoutAlignType being string is an example. please be aware of the type used in your case). - -Take a look at the directive name (in this case it is conceptCard). Now do a global search for e.g. `, -``` - -Note: If the syntax looks like `skillIds: '&'`, then just change it to: - -```js -@Input() skillIds: Array, -``` - -(`Array` is an example. please be aware of the type used in your case). - -Next, change all cases of `ctrl.getSkillIds()` to `this.skillIds`. (Notice the parentheses were also removed.) - -Take a look at the directive name (in this case it is conceptCard). Now do a global search for e.g. ``, `xyz` is a string or a variable? To clarify this ambiguity, we use interpolation for variables. Interpolation has 2 forms: - - * `` (The `[]` indicates that the string is a variable) - * `` - - We use the first method in all cases except when the variable is interspersed with other text. e.g. `` - -3. Do not use interpolation with properties marked with [prop], or events. These automatically assume that a variable is passed. - -##### The syntax for `=` - -In most cases, the `=` is the same as `<` when looked at from an Angular2+ perspective, so just follow the steps given for `<` migration. - -#### 5. Start separating the other functions - -Take a look at this example directive code: - -```js -var ctrl = this; -ctrl.isLastWorkedExample = function() { - return ctrl.numberOfWorkedExamplesShown === - ctrl.currentConceptCard.getWorkedExamples().length; -}; - -ctrl.showMoreWorkedExamples = function() { - ctrl.explanationIsShown = false; - ctrl.numberOfWorkedExamplesShown++; -}; - -ctrl.$onInit = function() { - ctrl.conceptCards = []; - ctrl.currentConceptCard = null; - ctrl.numberOfWorkedExamplesShown = 0; - ctrl.loadingMessage = 'Loading'; - ConceptCardBackendApiService.loadConceptCards( - ctrl.getSkillIds() - ).then(function(conceptCardObjects) { - conceptCardObjects.forEach(function(conceptCardObject) { - ctrl.conceptCards.push(conceptCardObject); - }); - ctrl.loadingMessage = ''; - ctrl.currentConceptCard = ctrl.conceptCards[ctrl.index]; - ctrl.numberOfWorkedExamplesShown = 0; - if (ctrl.currentConceptCard.getWorkedExamples().length > 0) { - ctrl.numberOfWorkedExamplesShown = 1; - } - // TODO(#8521): Remove when this directive is migrated to Angular. - $rootScope.$applyAsync(); - }); -}; -``` - -Look at all lines matching the pattern `ctrl.someVariable = function(...) {...`. - -In the component you made in step one, just create those functions without the `ctrl` and `= function`: - -```js -export class ConceptCardComponent implements OnInit { - -constructor( - private conceptCardBackendApiService: ConceptCardBackendApiService, - private conceptCardObjectFactory: ConceptCardObjectFactory -) {} - - ngOnInit() { - } - - isLastWorkedExample() { - } - - showMoreWorkedExamples() { - } - -} -``` - -#### 6. Create class members (variables) - -Till now we only looked at `ctrl.someVariable = function()`. Now we will look at all the other cases. Take a look at this directive code: - -```js -var ctrl = this; -ctrl.isLastWorkedExample = function() { - return ctrl.numberOfWorkedExamplesShown === - ctrl.currentConceptCard.getWorkedExamples().length; -}; - -ctrl.showMoreWorkedExamples = function() { - ctrl.explanationIsShown = false; - ctrl.numberOfWorkedExamplesShown++; -}; - -ctrl.$onInit = function() { - ctrl.conceptCards = []; - ctrl.currentConceptCard = null; - ctrl.numberOfWorkedExamplesShown = 0; - ctrl.loadingMessage = 'Loading'; - ConceptCardBackendApiService.loadConceptCards( - ctrl.getSkillIds() - ).then(function(conceptCardObjects) { - conceptCardObjects.forEach(function(conceptCardObject) { - ctrl.conceptCards.push(conceptCardObject); - }); - ctrl.loadingMessage = ''; - ctrl.currentConceptCard = ctrl.conceptCards[ctrl.index]; - ctrl.numberOfWorkedExamplesShown = 0; - if (ctrl.currentConceptCard.getWorkedExamples().length > 0) { - ctrl.numberOfWorkedExamplesShown = 1; - } - // TODO(#8521): Remove when this directive is migrated to Angular. - $rootScope.$applyAsync(); - }); -}; -``` - -Looking at all the other `ctrl.` declarations we find `ctrl.numberOfWorkedExamplesShown`, `ctrl.currentConceptCard`, `ctrl.explanationIsShown`, `ctrl.numberOfWorkedExamplesShown++``, `ctrl.conceptCards`, `ctrl.loadingMessage`, etc. - -Now we have to define them as class members. In order to do so just remove `ctrl.` from the front of the variable and add them to the class above the constructor. For example: - -```js -export class ConceptCardComponent implements OnInit { -numberOfWorkedExamplesShown: number = 0; -currentConceptCard: ConceptCard; -explanationIsShown: boolean = false; -conceptCards: Array; -loadingMessage: string = ''; - -constructor( - private conceptCardBackendApiService: ConceptCardBackendApiService, - private conceptCardObjectFactory: ConceptCardObjectFactory -) {} - - ngOnInit() { - } - - isLastWorkedExample() { - } - - showMoreWorkedExamples() { - } - -} -``` - -#### 7. Copy the contents of the functions - -Anything with `ctrl.` becomes `this.`. For example: - -```js -ctrl.isLastWorkedExample = function() { - return ctrl.numberOfWorkedExamplesShown === - ctrl.currentConceptCard.getWorkedExamples().length; -}; -``` - -becomes - -```js -isLastWorkedExample(): boolean { - return this.numberOfWorkedExamplesShown === - this.currentConceptCard.getWorkedExamples().length; -} -``` - -**Note the dependency injections also get the `this.` prefix.** - -In the controller.$OnInit function we have: - -```js -ConceptCardBackendApiService.loadConceptCards( - ctrl.getSkillIds() - ) -``` - -This will become: - -```js -this.conceptCardBackendApiService.loadConceptCards( - this.skillIds - ) -``` - -#### 8. Add downgrade statement - -Import downgradeComponent from '@angular/upgrade/static'. Then add the following downgrade statement to the end of the file: - -```js -angular.module('oppia').directive( - 'conceptCard', downgradeComponent( - {component: ConceptCardComponent})); -``` - -#### 9. Change the name of the file - -Rename the file from `*directive|controller.ts` to `*component.ts`. Import this component into the corresponding module page and add it in the `declarations` and `entryComponents`. You can find the corresponding module page as follows: - -* For directives in the pages folder, they will be in the same sub-folder as `*.module.ts` -* For directives in the components folder, the module page is `shared-component.module.ts` - - -### Migrating an HTML file - -This is the easier part of migration but still should be migrated carefully. Here are the migration patterns: - -#### Changing `<[ ... ]>` to `{{ ... }}` - -The interpolation in angular uses `{{ }}` to interpolate. So change `<[ ]>` to `{{ }}`. For example, `
  • <[credit]>
  • ` becomes `
  • {{ credit }}
  • `. - -#### Removing `$ctrl` - -By default in Angular, all the variables of the class you migrated are available in HTML (unlike AngularJS where variables were prefixed by $ctrl or had to attached to the $scope). Remove all `$ctrl.` from HTML. For example, `
  • <[$ctrl.credits]>
  • ` becomes `
  • {{ credit }}
  • `. - -#### Change `ng-if` to `*ngIf` - -For example, `
  • <[$ctrl.credits]>
  • ` becomes `
  • {{ credit }}
  • `. - -#### Change `ng-repeat` to `*ngFor` - -| AngularJS | Angular2+ | -|-----|------| -|`
    ` | `
    `| -|``| `
    `| -|`<[item.letter]>` | `{{ credit.letter }}` | -|`
      ` | `
        `| -|`
      • <[credit]>
      • ` | `
      • {{ name }}
      • `| -|`
      ` | `
    `| -|`
    `| `
    `| - -### Other tags - -* `ng-cloak`: Remove. - -* `ng-class`: Change to `ngClass`. - -* `ng-show`/`ng-hide`: Follow [GeeksForGeeks](https://www.geeksforgeeks.org/what-is-the-equivalent-of-ngshow-and-nghide-in-angular-2/). - -#### HTML tag attributes - -If you see any HTML attribute which looks like `
    `, then just change it to `
    `. - -If you `ng-src`/`ng-srcset`, change it to `[src]`/`[srcset]`. - -#### HTML events - -All the events in HTML are available in angular. Example `onClick` becomes `(click)`, `ng-click` becomes `(click)`, and `ng-submit` becomes `(ngSubmit)`. - -#### Translations - -You may come across the following: - -```html - -``` - -Convert it like this: - -```html - - -``` - -If there are no translate-values, simply use `"'I18N_VARIABLE_NAME' | translate"` - -Please note the single-quote marks around `I18N_VARIABLE_NAME`. - -#### CSS updates - -There may be some style updates required to make sure that the pages look exactly like before. You can find the changes here: https://github.com/oppia/oppia/pull/9980/files#diff-1d203da36aa74eef4c39b05a27eafbaeR40-R46. Besides this, styles that contain the directive name now need to be enclosed in a `
    ` tag. For example compare [this code from before migration](https://github.com/oppia/oppia/pull/9957/files#diff-25860f544f47c16a020aff8bb0c389fdL1-L3) to the [migrated code](https://github.com/oppia/oppia/pull/9957/files#diff-45cbfaec92adcc709712a85df070f455R1-R4). - -## Testing your Pull Request - -1. Ensure your frontend tests pass - - Python: - ```console - python -m scripts.run_frontend_tests - ``` - - Note: If your migrated service involves HTTP calls and when you run the frontend test your frontend test fail for some other service (One error that might pop is `Error: No pending request to flush !`) then go ahead and migrate the failing tests for the other service too. You might have guessed that in such a case we have migrated a service which is now making HTTP calls in Angular using HttpClient but some other service that is issuing HTTP requests to this service is still testing by making calls via AngularJS HTTP module (using $httpBackend). Go through this [PR #9029](https://github.com/oppia/oppia/pull/9029/files), wherein `question-creation.service` and `question-backend-api.service` are migrated to Angular and we went ahead to change relevant tests in `questions-list.service.spec`. - -2. Ensure there are no typescript errors: - - Python: - ```console - python -m scripts.typescript_checks - ``` - -3. Ensure there are no linting errors: - - Python: - ```console - python -m scripts.linters.run_lint_checks - ``` - -4. Test manually. See where the directive you have migrated is being used. You can do this by seeing where it's corresponding `selector` is being used. Then check whether functionality that you have implemented works as expected (like on the develop branch). Add a screen recording of the places where the directive is used when you open your PR! - -## Implementation details to refactor Object Factories - -### 1. Remove certain imports - -The following imports will no longer be required: - -```js -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -``` - -### 2. Change the file overview - -Change the file overview to not include the term Object Factory. Instead, replace it with the word "model". - -For example: - -| Before | After | -|--------|-------| -|`Factory for creating new frontend instances of ParamMetadata`|`Model class for creating new frontend instances of ParamMetadata`| - -### 3. Move functions from ObjectFactory class - -Locate the class in the file whose name is suffixed by ObjectFactory. Move all the functions from that ObjectFactory class (except the constructor) and add them to the other class in the file. Add `static` in front of all the functions you moved. - -Before: - -```js -export class ParamMetadata { - action: string; - paramName: string; - source: string; - sourceInd: string; - constructor( - action: string, paramName: string, source: string, sourceInd: string) { - this.action = action; - this.paramName = paramName; - this.source = source; - this.sourceInd = sourceInd; - } -} - -@Injectable({ - providedIn: 'root' -}) -export class ParamMetadataObjectFactory { - createWithSetAction( - paramName: string, source: string, sourceInd: string): ParamMetadata { - return new ParamMetadata( - ExplorationEditorPageConstants.PARAM_ACTION_SET, paramName, source, - sourceInd); - } - - createWithGetAction( - paramName: string, source: string, sourceInd: string): ParamMetadata { - return new ParamMetadata( - ExplorationEditorPageConstants.PARAM_ACTION_GET, paramName, source, - sourceInd); - } -} -``` - -After: - -```js -export class ParamMetadata { - action: string; - paramName: string; - source: string; - sourceInd: string; - constructor( - action: string, paramName: string, source: string, sourceInd: string) { - this.action = action; - this.paramName = paramName; - this.source = source; - this.sourceInd = sourceInd; - } - - static createWithSetAction( - paramName: string, source: string, sourceInd: string): ParamMetadata { - return new ParamMetadata( - ExplorationEditorPageConstants.PARAM_ACTION_SET, paramName, source, - sourceInd); - } - - static createWithGetAction( - paramName: string, source: string, sourceInd: string): ParamMetadata { - return new ParamMetadata( - ExplorationEditorPageConstants.PARAM_ACTION_GET, paramName, source, - sourceInd); - } -} - -@Injectable({ - providedIn: 'root' -}) -export class ParamMetadataObjectFactory { - -} -``` - -Next, remove the @Injectable and the object factory class. Specifically, remove the code that looks like this: - -```js -@Injectable({ - providedIn: 'root' -}) -export class ParamMetadataObjectFactory { - -} -angular.module('oppia').factory( - 'ParamMetadataObjectFactory', - downgradeInjectable(ParamMetadataObjectFactory)); - -``` - -### 4. Remove the imports and class listings / instances - -Remove any references to the object factory from: - -- angular-service.index.ts -- oppia-angular-root.component.ts -- UgradedServices.ts - -### 5. Rename the file - -The file you are working on will be named either `*-object.factory.ts` or `*ObjectFactory.ts`. You need to remove the object factory part and add .model.ts instead. For example, `PlaythroughObjectFactory.ts` should be renamed to `playthrough-object.model.ts and `skill-summary-object.factory.ts` should be renamed to `skill-summary.model.ts`. - -### 6. Change the import (as you have changed the name of the file) and its usage around the codebase. - -**Make sure to search the codebase for the function name to make sure you find all usages.** - -For example, let one of the functions that you moved before (in step 3) be `createWithGetAction`. - -#### Pattern 1: ParamMetadataObjectFactory.createWithGetAction(...) - -First, make sure `ParamMetadata` has been imported: - -```js -import ParamMetadata from param-metadata.model.ts -``` - -Next, change `ParamMetadataObjectFactory.createWithGetAction()` to `ParamMetadata.createWithGetAction(...)`. Do this for all functions, and then remove any other `ParamMetadataObjectFactory` references left in the file. - -#### Pattern 2. this.paramMetadataObjectFactory.createWithGetAction(...) - -Make sure that ParamMetadata has been imported. Then change `this.paramMetadataObjectFactory.createWithGetAction(...)` to `ParamMetadata.createWithGetAction(...)`. Do this for all functions, and remove `paramMetadataObjectFactory` from the constructor. - -### 7. Changing the spec file - -Each ``*-object.factory.ts` will have its corresponding spec file named `*-object.factory.spec.ts`. You will need to follow the procedure mentioned in step 6 (the previous step), to refactor the spec as well. Note that in spec file `ParamMetadataObjectFactory` could be shortened to `pmof`, so searching by the function name in the spec file will be more accurate. - -**PRs for reference: [#10701](https://github.com/oppia/oppia/pull/10701/), [#10713](https://github.com/oppia/oppia/pull/10713/).** - -## FAQ - -### Common Issues with Migrating Services - -1. Front-end tests fail. This can for various reasons, but the most common one is return types. You will get errors like: ‘a’ is not defined on an object of type ‘X’. Try console logging the object you are receiving actually has the property you’re calling and adjust accordingly. This will mostly happen with HttpResponse objects. - -### Common Issues with Migrating Directives - -1. Error like this: - - ```text - 'some-selector' is not a known element: - 1. If 'some-selector' is an Angular component, then verify that it is part of this module. - 2. 2. If 'some-selector' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. - ``` - - This can occur for a couple of reasons: - - * The corresponding external Angular module is not yet integrated into the codebase, e.g. For `ngModel`, you need `FormsModule`. - * It is another un-migrated directive. You need to wrap it in an Angular wrapper and import it into your current module. Do it via the shared component module. Use [#9237](https://github.com/oppia/oppia/pull/9237/files) for reference - * The Angular module has a different selector i.e `md-card` becomes `mat-card` - -### Why do we need @Injectable decorator? - -There are two reasons: - -* Our app is in hybrid state i.e. half Angular and half AngularJS, and we need to downgrade each of our services to AngularJS so that our application runs smoothly. -* To define a class as a service, Angular uses the @Injectable() decorator to provide the metadata that allows Angular to inject it into a component as a dependency. When we provide the service at the root level, Angular creates a single, shared instance of the service and injects it into any class that asks for it. Registering the provider in the @Injectable() metadata also allows Angular to optimize an app by removing the service from the compiled app if it isn't used. - -### Why do we need `$rootScope.$applyAsync` with HTTP requests? - -As you can see in the example here, the directive updates the value when the promise is resolved: - -```js -TopicViewerBackendApiService.fetchTopicData(ctrl.topicName).then( - function(topicDataDict) { - ctrl.topicId = topicDataDict.topic_id; - ctrl.canonicalStoriesList = topicDataDict.canonical_story_dicts; - ctrl.degreesOfMastery = topicDataDict.degrees_of_mastery; - ctrl.skillDescriptions = topicDataDict.skill_descriptions; - ctrl.subtopics = topicDataDict.subtopics; - $rootScope.loadingMessage = ''; - ctrl.topicId = topicDataDict.id; -``` - -Everything was working fine before the migration, but after migration, we noticed that all the values in the above-mentioned function were updated but not propagated to the corresponding HTML file. Searching online yielded [a Stack Overflow post](https://stackoverflow.com/a/21659051) which mentions that: - -> Yes, AngularJS's bindings are "turn-based", they only fire on certain DOM events and on calls to `$applyAsync/$digest`. There are some useful services like `$http` and `$timeout` that do the wrapping for you, but anything outside of that requires calls to either `$applyAsync` or `$digest`._ - -The `$digest` cycle is not running after we've upgraded `$http` to `HttpClient`, so we add `$rootScope.$applyAsync` to explicitly ask Angular to propagate the changes to our HTML. - -### What is TestBed? - -When a service has a dependent service, DI (dependency injector) finds or creates that dependent service. And if that dependent service has its own dependencies, DI finds or creates them as well. To quote from the Angular docs, "As a service tester, you must at least think about the first level of service dependencies but you can let Angular DI do the service creation and deal with constructor argument order when you use the TestBed testing utility to provide and create services." - -### Some Common Migration Queries - -1. What are Promises? - - Promises are exactly what they sound like. In the simplest words, they are a promise to the developer that things will work, what to do when they work, and also when they don’t work. - - They can have three states: - - * Resolved: The caller of the promise has executed as expected. - * Rejected: The caller of the promise didn’t execute as expected - * Pending: The caller is yet to be executed - - What exactly are the `resolve` and `reject` that promises accept? They are simply function calls. For example, `successCallback(abcd)` will give parameter `abcd` to the resolve function when the promise caller is called. - - How is it structured? A promise is called using a `.then()` statement after the function. Note that you cannot put this after any function, only one that returns a promise. Then functions follow. If there is only one function, it is the resolve function. If there are two functions, they are called resolve and reject, respectively. The reject function is used mostly for error handling and unexpected behaviour - - For more reading check out the [MDN Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)! - - -2. What is `rootScope`? - - Angular has scopes. `$scope` is a local scope, which is bound to exactly one controller. It can local properties and functions of the controller. `$rootScope` on the other hand, is the global scope and can be accessed from everywhere. - - What is `$rootScope.$applyAsync()` and why do we use it? `$rootScope.$applyAsync` is used to update the global properties and variables so that the new state can be used by the function where it is called. As to why it is not updated automatically, the reason is that the Angular DOM basically runs in cycles, and apply causes the changes to be saved. Mostly this is done automatically, but in some cases, we have to do it explicitly. Remember that this function will go in the resolve of the promise! This is because we want the variables to get to their new state in case of expected behaviour. - - For more reference, see [the AngularJS docs on scopes](https://docs.angularjs.org/guide/scope). - - -3. How do I assign types in the Angular file? - - Follow a trail. Some are really simple, but they all follow the same pattern. Keep following the variable through different references to see what type to assign. You can even use other references of that variable. For example, if you wanted to assign a type to a function that returned `WindowRef.nativeWindow`, you would go to the `window-ref.service` file and see that `nativeWindow` returned the `_window` object which had a type `Window`. - - Other times it might be obvious from the name. For example, `SkillList` is obviously an array of Skills. However, double-check these! - - The final way is to use a console log statement. This is good for complicated data types. Run the local development server and log the value whose type you want to know. You’ll see something of type Object with certain properties. Search for these properties in the codebase to find out what type it is! - -4. What are fakeAsync() and flushMicrotasks() and why do we use them? - - For this we need to understand why being synchronous is a problem for tests. Compilers don’t compile code line by line. Instead, they push processes into a queue as they come and the resulting processes are pushed once the first processes end. What this means is that a process whose parent was called earlier may be executed after another whose parent was called later due to how much time the parent processes took. To make an asynchronous function synchronous we use fakeAsync combined with flushMicrotasks. FakeAsync creates a fake asynchronous zone wherein you can control process flow. When flushMicrotasks is called, it flushes the task queues, i.e it waits for the processes to leave the queue before proceeding further. Then, the tests are consistent! - -5. What are MockServices/FakeServices that are in the codebase? - - MockServices are basically just used to imitate real services and provide functionality for tests via inorganically-made function copies of the service. These are faster but don’t test services, so be wary of using them. The reason we use them is that we want to want to test the current service, not the other service, so we just use a small shell to provide the functionality we want. - -6. How can I have constants shared across Angular and AngularJS code? - - The Angular 2+ constants file is named _*.constants.ts_ whereas the AngularJS equivalents of those constants must be in a separate file named _*.constants.ajs.ts_. The constants must be first declared in the Angular constants file and then be declared in the corresponding AngularJS constants file by importing the constants class from the Angular constants file and using that class's properties to declare the AngularJS equivalents. Then import the AngularJS constants class in the module and add it to the `providers` list of the `NgModule`. - - For example, if there is a constant named `SKILL_EDITOR_CONSTANT` that needs to be used in skill editor, then add that constant to the `SkillEditorConstants` class of the file _skill-editor-page.constants.ts_ like this: - - ```js - export class SkillEditorPageConstants { - ... - public static SKILL_EDITOR_CONSTANT = 'constant_value'; - ... - } - ``` - - Now, add the constant to the AngularJS file as well: - - ```js - import { SkillEditorPageConstants } from - 'pages/skill-editor-page/skill-editor-page.constants.ts'; - - ... - oppia.constant('SKILL_EDITOR_CONSTANT', SkillEditorPageConstants.SKILL_EDITOR_CONSTANT); - ... - ``` - - And now you can use the constant in both your AngularJS as well as Angular parts of the code! - -## Contact - -For any queries related to angular migration, please don't hesitate to reach out to **Srijan Reddy (@srijanreddy98)**. From 955f633f81929b171b6f646931151da79a997a4c Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:35:57 +0530 Subject: [PATCH 5/8] Update _Sidebar.md link removal as page does not exist --- _Sidebar.md | 1 - 1 file changed, 1 deletion(-) diff --git a/_Sidebar.md b/_Sidebar.md index 4d4a8cdc..cac8edca 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -108,7 +108,6 @@ * Frontend * [[How to define frontend types and test them|Guide-on-defining-types]] * [[Frontend file naming conventions|The-File-Naming-Convention-and-Directory-Structure]] - * [[Angular Migration|Angular-Migration]] * [[UX guidelines|Oppia-UX-guidelines-&-rationales]] * [[Writing style guide|Writing-style-guide]] * [[Schemas|Schemas]] From e9f214f73d58001bb87e0d1c9aac6c1a9a6e7d6d Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:41:25 +0530 Subject: [PATCH 6/8] Update Overview-of-the-Oppia-codebase.md fixed mainpage html which is not used anymore --- Overview-of-the-Oppia-codebase.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Overview-of-the-Oppia-codebase.md b/Overview-of-the-Oppia-codebase.md index c6603e32..6ee0f53f 100644 --- a/Overview-of-the-Oppia-codebase.md +++ b/Overview-of-the-Oppia-codebase.md @@ -160,9 +160,13 @@ sequenceDiagram Components define both the view (the layout of the page the user sees) and the controller logic that responds to user input. -Most pages begin with a `*.mainpage.html` file, for example `topic-editor-page.mainpage.html`. This HTML file contains a `` tag, which refers to the `topic-editor-page.component.html` file. +Pages in Oppia are implemented as Angular page components under `core/templates/pages/`. Each page typically consists of: -Both files are HTML, but `*.mainpage.html` is what the browser sees as the webpage, so it begins with ``, contains ``, and so on like a normal HTML page. `*.component.html` on the other hand is just defining a tag, so it won't have those lines. Instead, it could just begin with a `
    ` tag. +- A `*.component.ts` file that contains the component’s controller logic. +- A `*.component.html` file that defines the template (view). +- A `*.module.ts` file that declares the Angular module and its dependencies. + +When modifying or exploring a page, start from its Angular component (`*.component.ts`) and corresponding template (`*.component.html`). Alongside each component, you'll also find a `*.module.ts`, for example `topic-editor-page.module.ts`. This defines the component as an Angular module and specifies its dependencies. You'll also see a `*.import.ts` file, which imports third-party dependencies. From b8b6a6d38b8259f6c02241f0e8f98501fe790dda Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:43:23 +0530 Subject: [PATCH 7/8] Revert "Update Overview-of-the-Oppia-codebase.md" This reverts commit e9f214f73d58001bb87e0d1c9aac6c1a9a6e7d6d. --- Overview-of-the-Oppia-codebase.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Overview-of-the-Oppia-codebase.md b/Overview-of-the-Oppia-codebase.md index 6ee0f53f..c6603e32 100644 --- a/Overview-of-the-Oppia-codebase.md +++ b/Overview-of-the-Oppia-codebase.md @@ -160,13 +160,9 @@ sequenceDiagram Components define both the view (the layout of the page the user sees) and the controller logic that responds to user input. -Pages in Oppia are implemented as Angular page components under `core/templates/pages/`. Each page typically consists of: +Most pages begin with a `*.mainpage.html` file, for example `topic-editor-page.mainpage.html`. This HTML file contains a `` tag, which refers to the `topic-editor-page.component.html` file. -- A `*.component.ts` file that contains the component’s controller logic. -- A `*.component.html` file that defines the template (view). -- A `*.module.ts` file that declares the Angular module and its dependencies. - -When modifying or exploring a page, start from its Angular component (`*.component.ts`) and corresponding template (`*.component.html`). +Both files are HTML, but `*.mainpage.html` is what the browser sees as the webpage, so it begins with ``, contains ``, and so on like a normal HTML page. `*.component.html` on the other hand is just defining a tag, so it won't have those lines. Instead, it could just begin with a `
    ` tag. Alongside each component, you'll also find a `*.module.ts`, for example `topic-editor-page.module.ts`. This defines the component as an Angular module and specifies its dependencies. You'll also see a `*.import.ts` file, which imports third-party dependencies. From af4787a763a72a3352c2728d78352483272e9681 Mon Sep 17 00:00:00 2001 From: Rohan Malhotra <139499925+rohanmalhotracodes@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:50:20 +0530 Subject: [PATCH 8/8] Update coding style guide for Angular-only frontend --- Coding-style-guide.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Coding-style-guide.md b/Coding-style-guide.md index 3220ab10..a56cc810 100644 --- a/Coding-style-guide.md +++ b/Coding-style-guide.md @@ -310,8 +310,10 @@ _General note: We use the ES2017 standard for our JavaScript/TypeScript code. (S You can add a new custom type definitions if type casting is not possible. In the file `typings/custom-element-defs.d.ts`, we add a new property to `HTMLElement` by adding a custom type defintion. In this type casting cannot be used, since we are adding a new property to the existing type instead of changing it to some other type. ### Karma test specific guidelines -- Use `angular.mock.module` instead of `module` since the typings for angular-mocks does not support the usage of module. -- Use `angular.mock.inject` instead of `inject` to maintain a consistent behaviour. +- Use Angular testing utilities from `@angular/core/testing`, such as + `TestBed`, `waitForAsync`, and `fakeAsync`. +- Configure dependencies through `TestBed.configureTestingModule(...)` and + use `TestBed.inject(...)` to fetch injectable services in tests. ### When to add custom type defintions to the typings folder? - If you find a missing property in a typings package, create an issue [here](https://github.com/DefinitelyTyped/DefinitelyTyped) and a new file for the custom types with the issue link in the top of the file. @@ -320,12 +322,12 @@ _General note: We use the ES2017 standard for our JavaScript/TypeScript code. (S - If you add a property on scope defined in a link function, add it to `custom-scope-defs.d.ts` and add a comment specifying the filename for which it is added. - Make sure that all files have comments which explain why these custom type defintions are required and additional comments to explain each new added property if required. For example, `typings/custom-scope-defs.d.ts` has a top level comment explaining that the type defintions are needed for properties defined on scope in link function and then there are additional comments with properties added specifying which file they belong to. Go through the existing files and try to follow the same pattern when adding a new file. -### Component Directives -Usage of old-style AngularJS directives is discouraged. Instead, use component directives. Component directives are an advanced version of AngularJS directives and are more preferred because of the "isolated scope" it creates and the reusability it offers across modules. This is also the way forward in Angular 2+. +### Angular Components +Use Angular components for all new UI code. -- Do not create standalone controllers. The standalone controllers are those which are associated with the `ng-controller` directive in the HTML file. -- While creating a new directive, make sure to use the component directive instead of the old style directives. Now, here's something: The component directives create what is called an "isolated scope". So the component directive can be thought of as a reusable component not dependent on its surroundings and hence "isolated". Therefore you must not use `$scope` in the directive, except for some exceptions like `$scope.$on`, `$scope.$apply` and other internal functions of `$scope` which do not have a full replacement. Also `$uibmodal`s are exempted from this rule. -- There are many instances where this "isolated scope" needs to communicate with the surrounding, in such cases you must pass such data through the `bindToController` key of the component directive. This binds the values to the controller of the component directive and you can access those values in your directive's isolated scope. +- Do not create standalone controller-style logic in templates. +- Use `@Input()` and `@Output()` for component communication. +- Use directives only when component-based composition is not sufficient. ## Webpack