
JavaScript to Angular Easy Migration Guide: Part 2 - Redesigning the Plain JavaScript App as an Angular Application
Now that we have the mindset shifts in place, we can do the most important part of the migration: redesign the application.
This is where many teams cut corners. They copy the old logic into Angular files, keep the same coupling, add some types, and call it a migration. The result compiles, but it still behaves like a plain JavaScript app wearing Angular clothes.
In this part, we will take our Task Board app and redesign it properly.
Step 1: Describe the existing behavior clearly
Before redesigning, list what the app actually does.
The plain JavaScript Task Board supports:
- showing a list of tasks
- filtering by search text
- filtering by status
- selecting a task
- showing task details
- adding a new task
- editing an existing task
- deleting a task
- showing summary counts
- persisting tasks locally
This matters because architecture should be shaped around behavior, not around files.
Step 2: Identify route boundaries
Even a small app benefits from route thinking.
A beginner often treats routing as a late concern, but routes are major ownership boundaries.
For this teaching app, we can choose a simple route structure:
/tasksfor the board- optional room for later routes such as
/aboutor/settings
Could we keep everything on one route? Yes. But it is still useful to design the board as a route-level feature page because that mirrors how real Angular apps grow.
The route-level component for this feature will be TaskBoardPageComponent.
Its job is not to render every detail directly. Its job is to coordinate the feature and assemble the page from smaller components.
Step 3: Define the component taxonomy
A comprehensive redesign should classify components by role.
For this feature, a clean taxonomy looks like this:
Route/page component
TaskBoardPageComponent
Responsibilities:
- feature composition
- route-level coordination
- high-level layout decisions
Smart or coordinating components
For this small app, the page component can handle this role directly, but in a larger app you might split coordination further.
Presentational components
TaskToolbarComponentTaskStatsComponentTaskListComponentTaskListItemComponentTaskDetailsComponent
Responsibilities:
- receive data
- render UI
- emit user intentions
- avoid owning broad feature logic
Form component
TaskFormComponent
Responsibilities:
- manage the reactive form model
- validate input
- emit save or cancel actions
This taxonomy already improves the design because each component type has a different expected level of responsibility.
Step 4: Decide component communication paths
This is one of the most important design steps.
Let us list the interactions.
Toolbar changes filters
The toolbar can change:
- search text
- status filter
- “add task” intent
Possible designs:
- toolbar directly updates list and stats
- toolbar emits changes upward to the page
- toolbar writes into shared feature state
A good Angular design is either:
- parent-owned state with outputs, or
- a feature state service used by page and children
For this app, we will use a feature state service because several UI areas depend on the same state.
List item selection
When a user clicks a task, the selected task changes.
Possible designs:
- the list directly tells the detail component what to show
- the list emits
taskSelected - the selected ID lives in shared feature state
The correct design is to treat selection as shared feature state.
Form save and cancel
The form should not directly mutate the DOM or other components.
It should either:
- emit a save payload upward, or
- call a feature state service API through a thin coordinating layer
For teaching clarity, it is useful to let the form emit and let the page or state service handle persistence.
Delete action
Delete affects:
- the task list
- the stats
- the selected task if the deleted one was selected
- possibly the form if it was editing the same task
This is a good example of why direct component-to-component calls are brittle. Multiple parts of the UI care about the result, so the state change should happen in one place and the rest of the UI should derive from that source.
Step 5: Design the state layers
Now we decide where each piece of state should live.
Feature state service
A feature state service is perfect for this app because multiple components need to read and influence the same state.
The service can own:
- all tasks
- current search filter
- current status filter
- selected task ID
- whether the form is open
- which task is being edited
Derived state in the service
Using signals and computed state, the same service can derive:
- filtered tasks
- selected task
- counts by status
- whether the app is currently in add mode or edit mode
This is much cleaner than recalculating pieces manually in several places.
Local component state
Some local details can remain inside components, such as:
- transient visual details
- local field focus flags
- maybe a temporary expanded/collapsed section
The principle is simple: keep state as narrow as possible, but not narrower.
Step 6: Define the service responsibilities
A common beginner mistake is to create a giant service that does everything.
A more disciplined design splits responsibilities conceptually, even if a small tutorial app combines some of them for brevity.
For this app, think in terms of two conceptual services:
Task repository or data service
Responsibilities:
- load tasks
- save tasks
- persist to local storage or later to an API
Task board state service
Responsibilities:
- feature UI state
- selection
- filtering
- create/update/delete operations
- computed view state
In a small teaching app, these can be combined or one can depend on the other. The important lesson is to separate persistence concerns from view orchestration concerns.
Step 7: Define TypeScript models carefully
Now that the architecture is clearer, we can define models.
Domain model
export type TaskStatus = 'todo' | 'doing' | 'done';
export type TaskPriority = 'low' | 'medium' | 'high';
export interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
dueDate: string | null;
createdAt: string;
}
Create or update payloads
export interface TaskDraft {
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
dueDate: string | null;
}
This distinction helps because the form should not have to know about persistence-only fields like id or createdAt.
Filter model
export interface TaskFilters {
search: string;
status: 'all' | TaskStatus;
}
This improves clarity compared with scattering raw strings across the app.
Step 8: Design the reactive form boundary
The form component should receive input that tells it whether it is:
- creating a new task
- editing an existing task
It should initialize a reactive form based on that input and emit a typed save payload.
That gives us a clean contract.
Input ideas
- current draft values
- mode:
'create' | 'edit'
Output ideas
- save requested with
TaskDraft - cancel requested
- delete requested for current task if editing
The key lesson is that the form’s responsibility is the form model, not the entire board’s state.
Step 9: Decide what belongs in the URL and what does not
This is often skipped, but it matters in real apps.
For the teaching app, we could choose to keep filters purely in feature state. That is acceptable for a simple tutorial. But a more advanced design could store parts of the filter in query parameters so the URL becomes shareable.
This is a good teaching moment: not every state belongs in the URL, but state that users may want to bookmark or share often should.
For this tutorial, we will keep the URL simple and focus on feature-state migration, while acknowledging that a production app might push filters into query params.
Step 10: Design the template strategy
Now we can ask what should happen in templates versus TypeScript code.
Good template responsibilities
- display values
- bind inputs and outputs
- conditional rendering
- list rendering
- simple formatting
Bad template responsibilities
- complex filtering logic
- business rules
- multi-step calculations
- persistence behavior
The template should remain readable. If the template starts looking like a script, logic should move into the component or service.
Step 11: Identify anti-patterns from the old app that we will not carry over
This is crucial. Migration is not just about what you build. It is also about what you refuse to keep.
For the Task Board app, we explicitly reject these carryovers:
- direct DOM mutation for routine UI updates
- global mutable state object shared everywhere
- one mega component doing list, form, stats, and details all at once
- direct component-to-component calls across branches
- using
anyinstead of modeling data properly - reading truth from the DOM instead of from state
- duplicated derived state stored in multiple places
Writing this list down is powerful because it gives the team guardrails.
Step 12: Plan the migration sequence
Now that the redesign is ready, we can sequence the implementation.
A clean sequence is:
- create core models
- build the feature state service
- create the route/page shell
- build presentational components
- connect communication paths
- add the reactive form component
- connect local persistence
- add routing polish and quality rules
- review for coupling and state duplication
This sequence is better than translating one old file at a time because it respects the new architecture.
The redesign in one view
Here is the target design in plain words.
App structure
AppComponenthosts the router outlet/tasksroute rendersTaskBoardPageComponent
Feature state
TaskBoardStateServiceowns tasks, filters, selection, and edit mode- computed state provides filtered tasks, stats, and selected task
Communication
- toolbar updates filters through the page/service contract
- list emits or triggers selection via the state service contract
- details reads selected task state
- form receives editing context and emits save/cancel actions
Persistence
- a repository or storage-aware service reads and writes tasks
Types
- domain model, draft model, and filter model are distinct and explicit
This is now an Angular design. We have not even written much code yet, but the app has already become more understandable.
Why this part matters more than the coding part
Many beginners want to jump straight to code because code feels productive. But for migrations, redesign is where the most valuable decisions happen.
If you get the architecture wrong, good syntax will not save you.
If you get the architecture right, the code becomes easier to write, easier to test, and much easier to explain to someone else.
What comes next
In the final part, we will implement the Angular version fully and walk through:
- standalone Angular setup
- signals and computed state
- reactive forms
- routing
- quality rules
- performance and template considerations
- comparison with the original plain JavaScript version
At that point, you will be able to see not only how the app changed, but why the new structure is better for growth.
Leave a Reply
Your e-mail address will not be published. Required fields are marked *