<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Random stuff about software]]></title><description><![CDATA[Random stuff about software]]></description><link>https://blog.khairuddinniam.net</link><generator>RSS for Node</generator><lastBuildDate>Thu, 23 Apr 2026 00:01:05 GMT</lastBuildDate><atom:link href="https://blog.khairuddinniam.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Angular testing using vitest and testing-library]]></title><description><![CDATA[To test the angular project using vitest and testing-library, first, install the required libraries on devDependencies
- vite
- vitest 
- @testing-library/angular 
- @testing-library/jest-dom 
- jsdom 
- jest-preset-angular
- reflect-metadata
- @swc/...]]></description><link>https://blog.khairuddinniam.net/angular-testing-using-vitest-and-testing-library</link><guid isPermaLink="true">https://blog.khairuddinniam.net/angular-testing-using-vitest-and-testing-library</guid><category><![CDATA[Angular]]></category><category><![CDATA[vitest]]></category><category><![CDATA[Testing Library]]></category><dc:creator><![CDATA[Khairuddin Ni'am]]></dc:creator><pubDate>Sat, 18 Mar 2023 18:21:40 GMT</pubDate><content:encoded><![CDATA[<p>To test the angular project using vitest and testing-library, first, install the required libraries on devDependencies</p>
<pre><code class="lang-plaintext">- vite
- vitest 
- @testing-library/angular 
- @testing-library/jest-dom 
- jsdom 
- jest-preset-angular
- reflect-metadata
- @swc/core
- unplugin-swc
- @types/testing-library__jest-dom
</code></pre>
<p>Then we need to create <code>test-setup.ts</code> to initialize the testing environment. Zone js doesn't play well with vitest, so we need to make fake zone js (with empty implementation). Noop zone js implementation is from Younes Jaaidi's github, <a target="_blank" href="https://github.com/yjaaidi/experiments/blob/versatile-angular/src/app/testing/noop-zone.ts">here</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">'./noop-zone'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'jest-preset-angular/setup-jest'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'reflect-metadata'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'@testing-library/jest-dom'</span>
</code></pre>
<p>Configure vite.config.ts</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'vite'</span>
<span class="hljs-keyword">import</span> swc <span class="hljs-keyword">from</span> <span class="hljs-string">'unplugin-swc'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> {
    test: {
      environment: <span class="hljs-string">'jsdom'</span>,
      globals: <span class="hljs-literal">true</span>,
      setupFiles: <span class="hljs-string">'testing/test-setup.ts'</span>,
    },
    plugins: [
      swc.vite(),
    ],
    esbuild: <span class="hljs-literal">false</span>,
  }
})
</code></pre>
<p>Because we're using <code>globals: true</code>, we need to update tsconfig.ts types for typings</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"emitDecoratorMetadata"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"types"</span>: [
      <span class="hljs-string">"vitest/globals"</span>
    ]
  }
}
</code></pre>
<p>For further reference, you can see vitest documentation <a target="_blank" href="https://vitest.dev/config/#globals">here</a>.</p>
<p>Thats it. You can start writing tests. For source code, you can see it on <a target="_blank" href="https://github.com/khairuddinniam/blog-angular-testing">my github repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Angular Multiple Custom User Interface]]></title><description><![CDATA[In this post, we'll create an angular application that has many build configurations and each configuration has a different user interface but with the same business logic.
Let's say we already have an app. Then, our BMW client requests us to create ...]]></description><link>https://blog.khairuddinniam.net/angular-multiple-custom-user-interface</link><guid isPermaLink="true">https://blog.khairuddinniam.net/angular-multiple-custom-user-interface</guid><category><![CDATA[Angular]]></category><dc:creator><![CDATA[Khairuddin Ni'am]]></dc:creator><pubDate>Wed, 30 Nov 2022 19:18:13 GMT</pubDate><content:encoded><![CDATA[<p>In this post, we'll create an angular application that has many build configurations and each configuration has a different user interface but with the same business logic.</p>
<p>Let's say we already have an app. Then, our BMW client requests us to create a completely different user interface on the same app. To do this, we need to separate the business logic and view model from the UI component and then use the container component as a host to the UI component. The UI component is loaded based on the build configuration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669829196717/6YwhRgYqn.png" alt class="image--center mx-auto" /></p>
<p>For example, a home page route has 2 inputs (first name and last name), an output (full name), and a save button. The full name combines values from first name and last name. The save button is disabled until both first name and last name are filled.</p>
<pre><code class="lang-plaintext">- home-page.bmw.component.ts (BMW UI component)
- home-page.component.ts (container component)
- home-page.default.component.ts (default UI component)
- home-page.service.ts (service)
</code></pre>
<p><code>home-page.service.ts</code> contains the business logic and view model needed for the UI component.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// home-page.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { FormControl, FormGroup, Validators } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;
<span class="hljs-keyword">import</span> { combineLatest, map, startWith, Subscription } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>;

<span class="hljs-meta">@Injectable</span>({
  providedIn: <span class="hljs-string">'root'</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> HomePageService {

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) { }

  <span class="hljs-keyword">private</span> _subscription?: Subscription;

  formGroup = <span class="hljs-keyword">new</span> FormGroup({
    firstName: <span class="hljs-keyword">new</span> FormControl(<span class="hljs-string">''</span>, [Validators.required]),
    lastName: <span class="hljs-keyword">new</span> FormControl(<span class="hljs-string">''</span>, [Validators.required]),
    fullName: <span class="hljs-keyword">new</span> FormControl({
      value: <span class="hljs-string">''</span>,
      disabled: <span class="hljs-literal">true</span>
    }, [Validators.required])
  });

  vm$?: ReturnType&lt;HomePageService[<span class="hljs-string">'createViewModel'</span>]&gt;;

  init() {
    <span class="hljs-built_in">this</span>._subscription = <span class="hljs-keyword">new</span> Subscription();

    <span class="hljs-keyword">const</span> firstNameValue$ = <span class="hljs-built_in">this</span>.formGroup.controls.firstName.valueChanges.pipe(
      startWith(<span class="hljs-built_in">this</span>.formGroup.controls.firstName.value)
    );
    <span class="hljs-keyword">const</span> lastNameValue$ = <span class="hljs-built_in">this</span>.formGroup.controls.lastName.valueChanges.pipe(
      startWith(<span class="hljs-built_in">this</span>.formGroup.controls.lastName.value)
    );
    <span class="hljs-built_in">this</span>._subscription.add(
      combineLatest([
        firstNameValue$,
        lastNameValue$
      ]).subscribe(<span class="hljs-function">(<span class="hljs-params">[firstName, lastName]</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> fullName = <span class="hljs-built_in">this</span>.formGroup.controls.fullName;
        <span class="hljs-keyword">const</span> value = <span class="hljs-string">`<span class="hljs-subst">${firstName}</span> <span class="hljs-subst">${lastName}</span>`</span>.trim();
        fullName.setValue(value);
      })
    );

    <span class="hljs-built_in">this</span>.vm$ = <span class="hljs-built_in">this</span>.createViewModel();
  }

  reset() {
    <span class="hljs-built_in">this</span>._subscription?.unsubscribe();
    <span class="hljs-built_in">this</span>._subscription = <span class="hljs-literal">undefined</span>;
  }

  <span class="hljs-keyword">private</span> createViewModel() {
    <span class="hljs-keyword">const</span> state$ = {
      saveButtonDisabled: <span class="hljs-built_in">this</span>.formGroup.statusChanges.pipe(
        startWith(<span class="hljs-built_in">this</span>.formGroup.status),
        map(<span class="hljs-function">(<span class="hljs-params">status</span>) =&gt;</span> status === <span class="hljs-string">'INVALID'</span>)
      )
    };

    <span class="hljs-keyword">const</span> handler = {
      saveButton: {
        onClick: <span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Save button clicked'</span>)
        }
      }
    };

    <span class="hljs-keyword">const</span> vm$ = combineLatest(state$).pipe(
      map(<span class="hljs-function">(<span class="hljs-params">state</span>) =&gt;</span> ({
        saveButton: {
          disabled: state.saveButtonDisabled,
          onClick: handler.saveButton.onClick
        }
      }))
    );
    <span class="hljs-keyword">return</span> vm$;
  }
}
</code></pre>
<p><code>home-page.component.ts</code> serve as a <strong>container</strong>, using the <a target="_blank" href="https://angular.io/api/common/NgComponentOutlet">ngComponentOutlet</a> directive to load the UI component, and provide <strong>service</strong> for the UI component. <code>contentType</code> is loaded from <code>environment</code> which is different on each build configuration.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// home-page.component.ts</span>
<span class="hljs-keyword">import</span> { ChangeDetectionStrategy, Component, OnDestroy, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { CommonModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/environments/environment'</span>;
<span class="hljs-keyword">import</span> { HomePageService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./home-page.service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-home-page'</span>,
  standalone: <span class="hljs-literal">true</span>,
  imports: [CommonModule],
  providers: [HomePageService],
  template: <span class="hljs-string">`
    &lt;ng-container *ngComponentOutlet="contentType"&gt;&lt;/ng-container&gt;
  `</span>,
  styles: [
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> HomePageComponent <span class="hljs-keyword">implements</span> OnInit, OnDestroy {

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> service: HomePageService</span>) { }

  contentType = environment.uiComponents.homePage;

  ngOnInit(): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.service.init();
  }

  ngOnDestroy(): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.service.reset();
  }
}
</code></pre>
<p>The purpose of <code>home-page.default.component.ts</code> and <code>home-page.bmw.component.ts</code> is to render an HTML template. Business logic and view model came from service that is injected into the component. This component <code>should not</code> contain any logic in the template.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// home-page.default.component.ts</span>
<span class="hljs-keyword">import</span> { ChangeDetectionStrategy, Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { CommonModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;
<span class="hljs-keyword">import</span> { HomePageService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./home-page.service'</span>;
<span class="hljs-keyword">import</span> { ReactiveFormsModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-home-page.default'</span>,
  standalone: <span class="hljs-literal">true</span>,
  imports: [
    CommonModule, 
    ReactiveFormsModule
  ],
  template: <span class="hljs-string">`
    &lt;ng-container *ngIf="service.vm$ | async as vm"&gt;
      &lt;h1&gt;Default UI&lt;/h1&gt;
      &lt;div&gt;
        &lt;label for="name"&gt;First name: &lt;/label&gt;
        &lt;input id="name" type="text" [formControl]="service.formGroup.controls.firstName"&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;label for="name"&gt;Last name: &lt;/label&gt;
        &lt;input id="name" type="text" [formControl]="service.formGroup.controls.lastName"&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;label for="name"&gt;Full name: &lt;/label&gt;
        &lt;input id="name" type="text" [formControl]="service.formGroup.controls.fullName"&gt;
      &lt;/div&gt;
      &lt;button [disabled]="vm.saveButton.disabled" (click)="vm.saveButton.onClick()"&gt;Save button&lt;/button&gt;
    &lt;/ng-container&gt;
  `</span>,
  styles: [
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> HomePageDefaultComponent <span class="hljs-keyword">implements</span> OnInit {

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> service: HomePageService</span>) { }

  ngOnInit(): <span class="hljs-built_in">void</span> {
  }

}
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// home-page.bmw.component.ts</span>
<span class="hljs-keyword">import</span> { ChangeDetectionStrategy, Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { CommonModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;
<span class="hljs-keyword">import</span> { HomePageService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./home-page.service'</span>;
<span class="hljs-keyword">import</span> { ReactiveFormsModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-home-page.bmw'</span>,
  standalone: <span class="hljs-literal">true</span>,
  imports: [
    CommonModule, 
    ReactiveFormsModule
  ],
  template: <span class="hljs-string">`
    &lt;ng-container *ngIf="service.vm$ | async as vm"&gt;
      &lt;h1&gt;BMW UI&lt;/h1&gt;
      &lt;button [disabled]="vm.saveButton.disabled" (click)="vm.saveButton.onClick()"&gt;Save button&lt;/button&gt;
      &lt;div&gt;
        &lt;label for="name"&gt;Full name: &lt;/label&gt;
        &lt;input id="name" type="text" [formControl]="service.formGroup.controls.fullName"&gt;
      &lt;/div&gt;
      &lt;p&gt;Please fill form below&lt;/p&gt;
      &lt;div class="row"&gt;
        &lt;div class="column"&gt;
          &lt;label for="name"&gt;First name: &lt;/label&gt;
          &lt;input id="name" type="text" [formControl]="service.formGroup.controls.firstName"&gt;
        &lt;/div&gt;
        &lt;div class="column"&gt;
          &lt;label for="name"&gt;Last name: &lt;/label&gt;
          &lt;input id="name" type="text" [formControl]="service.formGroup.controls.lastName"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/ng-container&gt;
  `</span>,
  styles: [
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> HomePageBmwComponent <span class="hljs-keyword">implements</span> OnInit {

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> service: HomePageService</span>) { }

  ngOnInit(): <span class="hljs-built_in">void</span> {
  }

}
</code></pre>
<p>Below is the screenshot of the default build (<code>http://localhost:4200/home</code>) and the BMW build (<code>http://localhost:4300/home</code>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669835308668/n2nRear0k.gif" alt="Default user interface" class="image--center mx-auto" />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669835341476/ZZxr1TrFD.gif" alt="BMW user interface" class="image--center mx-auto" /></p>
<p>For the source code, you can see it on <a target="_blank" href="https://github.com/khairuddinniam/multiple-custom-ui">my GitHub</a>.</p>
]]></content:encoded></item></channel></rss>