Quantcast
Channel: Damir's Corner
Viewing all 484 articles
Browse latest View live

Staggered Animation in Ionic Angular

$
0
0

Recently, Josh Morony published an interesting tutorial about staggered animations in Ionic. Since he's using StencilJS, there are some syntax changes required to make his sample code work with Angular. It wasn't as trivial as I expected.

Staggered animation

His approach is based on CSS variables that are set in individual list items:

<ion-list lines="none">
  <ion-item style="--animation-order: 0">
    <ion-label>One</ion-label>
  </ion-item>
  <ion-item style="--animation-order: 1">
    <ion-label>Two</ion-label>
  </ion-item>
  <ion-item style="--animation-order: 2">
    <ion-label>Three</ion-label>
  </ion-item>
  <ion-item style="--animation-order: 3">
    <ion-label>Four</ion-label>
  </ion-item>
  <ion-item style="--animation-order: 4">
    <ion-label>Five</ion-label>
  </ion-item>
</ion-list>

The value of the variable is then used in CSS to parameterize the animation-delay:

ion-item {
  animation: popIn 0.2s calc(var(--animation-order) * 70ms) both ease-in;
}

@keyframes popIn {
  0% {
    opacity: 0;
    transform: scale(0.6) translateY(-8px);
  }

  100% {
    opacity: 1;
    transform: none;
  }
}

Of course, list items usually aren't hardcoded. Hence, a binding must be used to set the correct value to the CSS variable. Unfortunately, the ngStyle directive doesn't work with CSS variables:

<ion-list lines="none">
  <ion-item *ngFor="let item of items; index as i"
            [ngStyle]="{'--animation-order': i}">
    <ion-label>{{item}}</ion-label>
  </ion-item>
</ion-list>

Neither does style binding:

<ion-list lines="none">
  <ion-item *ngFor="let item of items; index as i"
            [style.--animation-order]="i">
    <ion-label>{{item}}</ion-label>
  </ion-item>
</ion-list>

I found a workaround for that in a related GitHub issue:

<ion-list lines="none">
  <ion-item *ngFor="let item of items; index as i"
      [attr.style]="sanitizer.bypassSecurityTrustStyle('--animation-order: ' + i)">
    <ion-label>{{item}}</ion-label>
  </ion-item>
</ion-list>

DomSanitizer must be injected into the page for this to work:

constructor(public sanitizer: DomSanitizer) {}

If you're using Ionic 5, you can update your project to Angular 9. This will allow you to use style binding syntax:

<ion-list lines="none">
  <ion-item *ngFor="let item of items; index as i"
            [style.--animation-order]="i">
    <ion-label>{{item}}</ion-label>
  </ion-item>
</ion-list>

However, even in Angular 9, the ngStyle directive still doesn't support CSS variables.

The full source code for the sample project is available in a Bitbucket repository.

This blog post is a part of a series of posts about animations in Ionic.


Scroll-dependent Animation in Ionic

$
0
0

In my previous blogpost, I implemented a staggered animation in Ionic Angular where the animation-delay depended only on the position of the item in the list. This time, the delay will depend on the current scroll position of the list. The animation will start in the middle of the screen and move towards the top and bottom edges.

Animation originating from the middle of the screen

Since this will require inspecting the scroll position and calculating how each item in the list is affected, I will only do the calculation once, immediately before triggering the animation instead of continuously whenever the scroll position changes. In my case, this will be just before navigating to a different page:

public pushDetail() {
  this.initializeAnimation().then(() => {
    this.navController.navigateForward('/detail');
  });
}

To implement the animation, I will need the following data for each item in the list:

interface ItemAnimationData {
  visible: boolean;
  position: 'above' | 'below'; // relative to the middle of the screen
  offset: number;
}

Although the calculations are not particularly complicated, they still require a non-trivial amount of code:

@ViewChild(IonContent)
private ionContent: IonContent;

@ViewChild(IonList, { read: ElementRef })
private ionList: ElementRef;

@ViewChildren(IonItem, { read: ElementRef })
private ionItems: QueryList<ElementRef>;

private initializeAnimation() {
  return this.ionContent.getScrollElement().then(scrollElement => {
    const minYVisible = scrollElement.scrollTop;
    const maxYVisible = scrollElement.scrollTop + scrollElement.offsetHeight;
    const midYVisible = (maxYVisible + minYVisible) / 2;
    const listTop = this.ionList.nativeElement.offsetTop;

    let firstIndexBelow;
    const itemPositions: Pick<ItemAnimationData, 'visible' | 'position'>[] = 
      this.ionItems.map((item, index) => {
        const elementTop = listTop + item.nativeElement.offsetTop;
        const elementBottom = listTop + item.nativeElement.offsetTop +
          item.nativeElement.offsetHeight / 2;
        const elementMiddle = (elementTop + elementBottom) / 2;
        const visible = elementBottom > minYVisible && elementTop < maxYVisible;
        const position = elementMiddle < midYVisible ? 'above' : 'below';
        if (position === 'below' && firstIndexBelow === undefined) {
          firstIndexBelow = index;
        }
        return { visible, position };
      });

    this.itemAnimationData = itemPositions.map((item, index) => {
      return {
        ...item,
        offset: item.position === 'above' ?
          firstIndexBelow - index - 1 :
          index - firstIndexBelow
      };
    });
  });
}

I think the code is best explained with the following diagram, depicting the meaning of each variable and field:

Diagram of variables and fields from the sample code above

The calculated values in ItemAnimationData are bound to a CSS class and variable in the template:

<ion-list lines="none">
  <ion-item *ngFor="let item of items; index as i"
    (click)="pushDetail()"
    [style.--animation-offset]="itemAnimationData && itemAnimationData[i].offset || 0"
    [ngClass]="{'pop-out': itemAnimationData && itemAnimationData[i].visible}">
    <ion-label>{{item}}</ion-label>
  </ion-item>
</ion-list>

The style binding syntax only work with CSS variables since Angular 9. You can find a working alternative syntax for Angular 8 in my previous blogpost.

The animation takes advantage of the CSS variable and is defined as follows:

.pop-out {
  animation: popOut 0.2s calc(var(--animation-offset) * 70ms) both ease-in;
}

@keyframes popOut {
  0% {
    opacity: 1;
    transform: none;
  }

  100% {
    opacity: 0;
    transform: scale(0.6) translateY(-8px);
  }
}

This is enough for the animation to work as expected but there are few more details to take care of because I'm using it in combination with a page transition.

Most importantly, the pop-out CSS class must be cleared after the page transition. Otherwise, the animation will trigger once again when we navigate back to this page. The best place to do this is in the ionViewDidLeave lifecycle hook:

public ionViewDidLeave() {
  this.itemAnimationData = undefined;
}

Another nice-to-have improvement is to delay the page transition until the animation is complete. To do that, we need the information about the total duration of the animation in the code. To avoid repeating the duration values, they can be defined in the code:

public animationDuration = 200;
public animationDelay = 70;

Then bound in the template:

<ion-list lines="none">
  <ion-item *ngFor="let item of items; index as i"
    (click)="pushDetail()"
    [style.--animation-duration.ms]="animationDuration"
    [style.--animation-delay.ms]="animationDelay"
    [style.--animation-offset]="itemAnimationData && itemAnimationData[i].offset || 0"
    [ngClass]="{'pop-out': itemAnimationData && itemAnimationData[i].visible}">
    <ion-label>{{item}}</ion-label>
  </ion-item>
</ion-list>

And finally used in the CSS:

.pop-out {
  animation: popOut var(--animation-duration) calc(var(--animation-offset) * var(--animation-delay)) both ease-in;
}

The total duration can be calculated from these values and the maximum delay multiplier for a visible list item:

public pushDetail() {
  this.initializeAnimation().then(() => {
    const maxVisibleAnimationOffset = this.itemAnimationData
      .filter(item => item.visible)
      .map(item => item.offset)
      .reduce((acc, cur) => Math.max(acc, cur));

    setTimeout(() => {
      this.navController.navigateForward('/detail');
    }, maxVisibleAnimationOffset * this.animationDelay + this.animationDuration);
  });
}

By delaying the page navigation for the calculated amount of time, the user will see the full list item animation before the page transition starts.

The full source code for the sample project is available in a Bitbucket repository.

This blog post is a part of a series of posts about animations in Ionic.

Customizing Page Transitions in Ionic 5

$
0
0

Although Ionic supports custom transitions when navigating between pages, I couldn't find much documentation about it. However, by combining information from different sources I managed to create one and fully understand the code involved.

Custom Ionic page transition

The default page transition can be easily replaced through Ionic Config. The custom animation can be specified as a value for the navAnimation field:

@NgModule({
  // ...
  imports: [
    BrowserModule,
    IonicModule.forRoot({ navAnimation: pageTransition }),
    AppRoutingModule
  ],
  // ...
})
export class AppModule {}

This bit is documented but the documentation doesn't explain what should be passed as the value. The best starting points are the default Android and iOS page transitions. The value expected by the navAnimation field is the animation factory function with the following signature:

function pageTransition(_: HTMLElement, opts: TransitionOptions): Animation;

Since Ionic doesn't seem to export the TransitionOptions in the signature above, you'll need to copy it into your application from Ionic source code:

export interface TransitionOptions extends NavOptions {
  progressCallback?: ((ani: Animation | undefined) => void);
  baseEl: any;
  enteringEl: HTMLElement;
  leavingEl: HTMLElement | undefined;
}

The same goes for the getIonPageElement() helper function that's used in both Android and iOS default page transitions:

export const getIonPageElement = (element: HTMLElement) => {
  if (element.classList.contains('ion-page')) {
    return element;
  }

  const ionPage = element.querySelector(
    ':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs'
  );
  if (ionPage) {
    return ionPage;
  }
  // idk, return the original element so at least something animates
  // and we don't have a null pointer
  return element;
};

Since the default Android page transition is much simpler from the iOS one, I based my custom page transition on its source code. After some refactoring, I came up with the following well-structured function (inline comments explain what individual parts do) which allows for easy modifications to the transition animation:

export function pageTransition(_: HTMLElement, opts: TransitionOptions) {
  const DURATION = 300;

  // root animation with common setup for the whole transition
  const rootTransition = createAnimation()
    .duration(opts.duration || DURATION)
    .easing('cubic-bezier(0.3,0,0.66,1)');

  // ensure that the entering page is visible from the start of the transition
  const enteringPage = createAnimation()
    .addElement(getIonPageElement(opts.enteringEl))
    .beforeRemoveClass('ion-page-invisible');

  // create animation for the leaving page
  const leavingPage = createAnimation().addElement(
    getIonPageElement(opts.leavingEl)
  );

  // actual customized animation
  if (opts.direction === 'forward') {
    enteringPage.fromTo('transform', 'translateX(100%)', 'translateX(0)');
    leavingPage.fromTo('opacity', '1', '0.25');
  } else {
    leavingPage.fromTo('transform', 'translateX(0)', 'translateX(100%)');
    enteringPage.fromTo('opacity', '0.25', '1');
  }

  // include animations for both pages into the root animation
  rootTransition.addAnimation(enteringPage);
  rootTransition.addAnimation(leavingPage);
  return rootTransition;
}

The code uses the new Ionic Animations API which was recently officially announced. That's the main reason why it only works with Ionic 5.

However, the same APIs were already used internally in Ionic 4. If you haven't upgraded to Ionic 5 yet, you should still be able to use the above code with only minor modifications. To see what's different, you can check the source code for the default animations in the version of Ionic you're using.

The full source code for the sample project is available in a Bitbucket repository.

This blog post is a part of a series of posts about animations in Ionic.

Using BlurHash in Ionic

$
0
0

BlurHash is a compact representation for image placeholders developed by Wolt. Implementations are available for many languages. Since TypeScript is one of them, it's easy to use in an Ionic application as well.

BlurHash placeholder transitioning to the final image

The instructions for the TypeScript implementation bring you a long way.

First, you need to install the library

npm i blurhash

Next, you implement the code for decoding the BlurHash value into a placeholder image:

private decodeBlurHash() {
  if (this.canvas && this.blurHash) {
    const context = this.canvas.nativeElement.getContext('2d');
    const imageData = context.createImageData(this.canvasWidth, this.canvasHeight);
    const pixels = decode(this.blurHash, this.canvasWidth, this.canvasHeight);
    imageData.data.set(pixels);
    context.putImageData(imageData, 0, 0);
  }
}

I've created a component for images with BlurHash placeholders so that's where the members used in the code above are declared:

export class BlurhashComponent implements AfterViewInit {

  private blurHashValue: string;
  @Input()
  get blurHash(): string {
    return this.blurHashValue;
  }
  set blurHash(value: string) {
    this.blurHashValue = value;
    this.decodeBlurHash();
  }

  private imageSrcValue: string;
  @Input()
  get imageSrc(): string {
    return this.imageSrcValue;
  }
  set imageSrc(value: string) {
    this.imageSrcValue = value;
  }

  @Input()
  public imageSrc: string;

  public imageLoaded = false;

  @ViewChild('canvas', {static: true})
  private canvas: ElementRef<HTMLCanvasElement>;

  public canvasWidth = 32;
  public canvasHeight = 32;

  public ngAfterViewInit(): void {
    this.decodeBlurHash();
  }

  // ...
}

The template consists of a canvas element for the placeholder and the img element for the final image:

<canvas #canvas [width]="canvasWidth" [height]="canvasHeight"></canvas>
<img [src]="imageSrc" (load)="imageLoaded = true" 
     [ngClass]="{'img-loaded': imageLoaded}">

The canvas element has a fixed small size as recommended and is expanded as necessary using CSS to ensure that its size matches the size of the img element:

:host {
    display: block;
    position: relative;
}

canvas {
    width: 100%;
    height: 100%;
    position: absolute;
}

img {
    opacity: 0;
    width: 100%;
    height: 100%;
    position: absolute;
}

The load event on the img element is triggered when the image is loaded. In our case, it adds a CSS class to the element to make it visible through a simple transition animation:

.img-loaded {
    animation: popIn 0.4s both ease-in;
}

@keyframes popIn {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

To use the component, only the BlurHash value and the image URL must be specified:

<app-blurhash blurHash="L[BO5qWra#j[pMoeoffkkEaxWXj@" [imageSrc]="imageSrc">
</app-blurhash>

Both of them will typically be sent to the client as a response to an API call. The BlurHash value will be generated from the image (or its thumbnail) in server-side code when images are imported into the system. For testing purposes, the encoder is also available on the BlurHash web site.

The correct size of the component must be ensured by applying CSS to it. This can be either fixed width and height or a fixed aspect ratio leaving the final size to the parent elements:

app-blurhash {
  width: 100%;
  padding-bottom: 75%
}

That's it. You can see the final result in the animation at the top of the blog post.

The full source code for the sample project is available in a Bitbucket repository.

This blog post is a part of a series of posts about animations in Ionic.

Content-based Modal Transitions in Ionic

$
0
0

Although transition animations for modal pages can be customized individually for each instance of a modal page, they still can't easily affect the content of the page that's opening them. The animation factory function only gets access to the modal page that's being opened but not to the originating page.

Despite this limitation, it's still possible to create transitions like the following one in Ionic:

Modal transition animation manipulating the content of the originating page

However, this transition isn't implemented as a single modal animation. I don't think that would even be possible. Instead, it consists of two parts:

  • Most of the animation is happening on the originating page before the modal is even created.
  • Only a minor part at the end of the animation is implemented as a modal transition animation.

The selected image is moved to the top of the page using the concept named FLIP animation. I've first heard about it in a Josh Morony's video. Paul Lewis provides a much more detailed explanation of the concept in his blog post. In this post, I'll focus on how I applied the principle to my animation. If you have any difficulties following my explanation, I suggest you check the two resources I linked earlier in the paragraph.

The images in my animation are Ionic Cards. Clicking one of them opens the modal page and triggers the corresponding transition animation:

<ion-card *ngFor="let image of images; index as i" (click)="openModal(i)">
  <img #imageElement [src]="image">
</ion-card>

To define the animation, I start by determining the absolute starting position of the image to animate (First in FLIP):

const imageElement = this.imageElements.toArray()[index].nativeElement;
const firstRect = imageElement.getBoundingClientRect();

I use the ViewChildren decorator to get a reference to the img elements on the page:

@ViewChildren('imageElement', { read: ElementRef })
private imageElements: QueryList<ElementRef<HTMLElement>>;

Then, I determine the ending position of the image to animate (Last in FLIP):

const clone = imageElement.cloneNode(true) as HTMLElement;
this.pageRef.nativeElement.appendChild(clone);
clone.classList.add('last-pos');
const lastRect = clone.getBoundingClientRect();

Of course, I must apply the final position to the element before I can read it. I do that by assigning the following CSS class to it:

img.last-pos {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 10;
}

You can also notice that I'm creating a (deep) clone of the element. This is to prevent the parent card from collapsing because it would have no more content after I removed the image from it. The pageRef field is a reference to the page element which is injected into the constructor:

constructor(
  private modalCtrl: ModalController,
  private pageRef: ElementRef<HTMLElement>
) {}

To give an illusion that the original image is moving and not its clone, I hide the original card:

const cardElement = this.cardElements.toArray()[index].nativeElement;
cardElement.classList.add('hidden');

Again, I get the reference to the cards with the ViewChildren decorator:

@ViewChildren(IonCard, {read: ElementRef})
private cardElements: QueryList<ElementRef<HTMLElement>>;

And the CSS class simply sets the opacity:

ion-card.hidden {
  opacity: 0;
}

From the starting and the ending position of the image I can calculate the transform that needs to be applied to the ending position to move the image back to its starting position (Invert in FLIP):

const invert = {
  translateX: firstRect.left - lastRect.left,
  translateY: firstRect.top - lastRect.top,
  scaleX: firstRect.width / lastRect.width,
  scaleY: firstRect.height / lastRect.height
};

Using these data, I can now create an Ionic animation for the image and play it (Play in FLIP):

const imageAnimation = createAnimation()
  .addElement(clone)
  .duration(300)
  .easing('ease-in-out')
  .beforeStyles({
    'transform-origin': '0 0'
  })
  .fromTo(
    'transform',
    `translate(${invert.translateX}px, ${invert.translateY}px) scale(${invert.scaleX}, ${invert.scaleY})`,
    'translate(0, 0) scale(1, 1)'
  );

await imageAnimation.play();

Once this animation plays to the end, it's time to open the modal and play its custom transition animation that continues from what was animated so far:

await this.playAnimation(index);

const modal = await this.modalCtrl.create({
  component: ModalPage,
  componentProps: { 
    image: this.images[index]
  },
  enterAnimation: modalEnterAnimation
});
await modal.present();

The default modal transition must be simplified to better match the ending state of the originating page. The image at the top is already at the correct position which means that only the rest of the page needs to fade in, without any transformations. It's best to start with the default animation and modify it as necessary. I've described the process in detail for regular page transition animations in a previous blog post. Here's the final result:

import { createAnimation, Animation } from '@ionic/core';

export function modalEnterAnimation(rootElement: HTMLElement): Animation {
  return createAnimation()
    .addElement(rootElement.querySelector('.modal-wrapper'))
    .easing('ease-in-out')
    .duration(300)
    .beforeStyles({transform: 'none'})
    .fromTo('opacity', 0, 1);
}

If you close the modal, you'll notice that the underlying page remained in its state from the end of the animation. You don't want that. In an ideal situation, you could create a reverse animation for closing the modal. But for this example, I decided to only reset the state of the page.

I returned the required actions as a lambda function from the playAnimation method:

return () => {
  clone.remove();
  cardElement.classList.remove('hidden');
};

I called it after the modal transition ended, i.e. after the promise returned by its present method resolved:

const resetAnimation = await this.playAnimation(index);

const modal = await this.modalCtrl.create({
  component: ModalPage,
  componentProps: { 
    image: this.images[index]
  },
  enterAnimation: modalEnterAnimation
});
await modal.present();

resetAnimation();

In the video at the beginning of the post, the other cards on the page also move off the screen so that the fade-in of the text on the modal page looks nicer. To achieve that, I created another animation for the ion-content element:

constructor(
  private modalCtrl: ModalController,
  private pageRef: ElementRef<HTMLElement>
) {}

const contentAnimation = createAnimation()
  .addElement(this.contentElement.nativeElement)
  .fromTo(
    'transform',
    'translateX(0)',
    `translateX(-${this.contentElement.nativeElement.offsetWidth}px)`
  );

I add both animations into a common parent one which I then play instead of each one separately. I also moved the common duration and easing configuration into the parent:

const parentAnimation = createAnimation()
  .duration(300)
  .easing('ease-in-out')
  .addAnimation([imageAnimation, contentAnimation]);

await parentAnimation.play();

To reset the changes made by the animation, I added a call to its stop method to the existing lambda for resetting the state of the page:

return () => {
  clone.remove();
  cardElement.classList.remove('hidden');
  parentAnimation.stop();
};

With all the changes applied, this is the final state of the playAnimation method:

private async playAnimation(index: number) {

  const imageElement = this.imageElements.toArray()[index].nativeElement;
  const firstRect = imageElement.getBoundingClientRect();

  const clone = imageElement.cloneNode(true) as HTMLElement;
  this.pageRef.nativeElement.appendChild(clone);
  clone.classList.add('last-pos');
  const lastRect = clone.getBoundingClientRect();

  const invert = {
    translateX: firstRect.left - lastRect.left,
    translateY: firstRect.top - lastRect.top,
    scaleX: firstRect.width / lastRect.width,
    scaleY: firstRect.height / lastRect.height
  };

  const imageAnimation = createAnimation()
  .addElement(clone)
  .beforeStyles({
    'transform-origin': '0 0'
  })
  .fromTo(
    'transform',
    `translate(${invert.translateX}px, ${invert.translateY}px) scale(${invert.scaleX}, ${invert.scaleY})`,
    'translate(0, 0) scale(1, 1)'
  );

  const cardElement = this.cardElements.toArray()[index].nativeElement;
  cardElement.classList.add('hidden');

  const contentAnimation = createAnimation()
  .addElement(this.contentElement.nativeElement)
  .fromTo(
    'transform',
    'translateX(0)',
    `translateX(-${this.contentElement.nativeElement.offsetWidth}px)`
  );

  const parentAnimation = createAnimation()
  .duration(300)
  .easing('ease-in-out')
  .addAnimation([imageAnimation, contentAnimation]);

  await parentAnimation.play();

  return () => {
    clone.remove();
    cardElement.classList.remove('hidden');
    parentAnimation.stop();
  };
}

The concepts it demonstrates should be a good starting point for whatever transition animations you need to create.

The full source code for the sample project is available in a Bitbucket repository.

This blog post is a part of a series of posts about animations in Ionic.

Window Positioning with AutoHotkey

$
0
0

When looking for a way to quickly position an application window at an exact predefined position, I chose AutoHotkey as the best tool for the job. The ability to create a script that moves a window and assign it to a keyboard shortcut was exactly what I needed. It still required some tweaking before it worked well enough for my needs.

I started with the following simple script based on an article I found:

ResizeWin(Left = 0, Top = 0, Width = 0, Height = 0)
{
    WinGetPos,X,Y,W,H,A
    If %Width% = 0
        Width := W

    If %Height% = 0
        Height := H

    WinMove,A,,%Left%,%Top%,%Width%,%Height%
}

#!F2::ResizeWin(1080,0,1280,720)

Although it worked great for some windows (e.g. Visual Studio Code, Visual Studio 2019), it positioned others (e.g. Total Commander, Firefox, OneNote) with a few pixels of offset. This is explained best with the following screenshot (the bottom window is positioned correctly):

AutoHotkey WinMove positioned some windows incorrectly

Further research revealed that this is a known issue with Desktop Window Manager based themes introduced in Windows Vista. The WinGetPosEx function attached to the linked post returns the horizontal and vertical offset values so that the positioning can be adjusted accordingly.

Even that didn't fix the issue for all applications because the offsets weren't symmetrical. Fortunately, the post linked to an alternative implementation of the same function that returns offsets separate for each side. Using the values it returns, I managed to update my function to work correctly for all application windows:

ResizeWin(Left = 0, Top = 0, Width = 0, Height = 0)
{
    ; restore the  window first because maximized window can't be moved
    WinRestore,A

    ; get the active window handle for the WinGetPosEx call
    WinGet,Handle,ID,A

    ; get the offsets
    WinGetPosEx(Handle,X,Y,W,H,Offset_Left,Offset_Top,Offset_Right,Offset_Bottom)

    If %Width% = 0
        Width := W

    If %Height% = 0
        Height := H

    ; adjust the position using the offsets
    Left -= Offset_Left
    Top -= Offset_Top
    Width += Offset_Left + Offset_Right
    Height += Offset_Top + Offset_Bottom

    ; finally position the window
    WinMove,A,,%Left%,%Top%,%Width%,%Height%
}

#!F2::ResizeWin(1080,0,1280,720)

; import the function from a file in the same folder
#include WinGetPosEx.ahk

In the script above, there's only a single keyboard shortcut defined but this can be easily extended with additional hotkeys for alternate window positions.

Book Review: Enterprise Application Patterns Using Xamarin.Forms

$
0
0

Enterprise Application Patterns using Xamarin.Forms

The Enterprise Application Patterns using Xamarin.Forms book by David Britch is available as a free download on the Microsoft's .NET Architecture Guides website. It's a short book with just under 100 pages, accompanied by a sample application implementing the described architectural approaches. The latter is updated more regularly than the book. It's therefore slightly out-of sync but not enough to be a serious issue.

The book focuses on various aspects of the MVVM (model-view-viewmodel) architectural pattern which is strongly recommended for all applications using Xamarin.Forms or other XAML based UI frameworks. No MVVM libraries are used. Instead, all the helper classes that are necessary to efectively use MVVM with Xamarin.Forms are implemented in the sample application and explained in the book. This will give the readers a better understanding of what is happening under the hood when they use an MVVM library of choice in their application. There's a big emphasis on decoupling different parts of the application and the benefits it brings, including easier unit testing.

In addition to MVVM-related topics, the book includes detailed coverage of interaction with REST services including authentication, caching, and even the circuit-breaker pattern. There's also a chapter and a half about the server-side implementation. Although that's not directly related to Xamarin.Forms or mobile applications, it can be helpful in better understanding of related client-side concerns and isn't that much of a distraction.

What I missed the most was any kind of guidance on how to structure business/domain logic in the mobile application. To some extent, this makes sense because the sample application has almost no business logic of its own. It mostly just interacts with REST services. However, this is not necessarily true for all mobile applications. Therefore, a chapter or two about domain layer architecture inside a mobile application would be a welcome addition.

I found the book to be a good introduction to MVVM. It can also serve as a refresher for someone with past MVVM experience who hasn't worked with Xamarin.Forms before. Although the sample code uses Xamarin.Forms, it's almost just as useful to WPF and UWP developers.

Matching Generic Type Arguments with Moq

$
0
0

The Moq mocking library in version 4.13.0 added support for matching generic type arguments when mocking generic methods. The documentation doesn't go into much detail but thanks to additional information in IntelliSense tooltips and the originating GitHub issue I managed to quickly resolve the no implicit reference conversion error which I encountered at first.

I wanted to use this new functionality to test navigation in my FreshMvvm page model. When a command is called, it should navigate to a different page (corresponding to the given page model):

public class SamplePageModel : FreshBasePageModel
{
  public ICommand PushSecondPageCommand { get; }

  public SamplePageModel()
  {
    PushSecondPageCommand = new Command(
      () => CoreMethods.PushPageModel<SecondPageModel>()
    );
  }
}

In the test, I needed to mock the PushPageModel method and verify that it was called with the correct page model class as its type argument:

[Test]
public void PushSecondPageCommandShouldPushSecondPage()
{
  var coreMethodsMock = new Mock<IPageModelCoreMethods>();

  // add Setup call for the PushPageModel method

  var pageModel = new SamplePageModel();
  pageModel.CoreMethods = coreMethodsMock.Object;

  pageModel.PushSecondPageCommand.Execute(null);

  coreMethodsMock.VerifyAll();
}

The It.isSubType<T> type matcher seemed a good fit for my needs. It should match any subtype of the type argument T including that type itself. I came up with the following Setup call (the It.Is<bool> matcher is used to match the optional method parameter):

coreMethodsMock
  .Setup(m => m.PushPageModel<It.IsSubtype<SecondPageModel>>(
    It.Is<bool>(b => b)))
  .Returns(Task.CompletedTask)
  .Verifiable();

Unfortunately, this resulted in a compiler error:

The type 'Moq.It.IsSubtype' cannot be used as type parameter 'T' in the generic type or method 'IPageModelCoreMethods.PushPageModel(object, bool, bool)'. There is no implicit reference conversion from 'Moq.It.IsSubtype' to 'FreshMvvm.FreshBasePageModel'.

Thinking about it, the error made perfect sense. The generic type argument is constrained to subtypes of FreshBasePageModel and the It.IsSubtype<T> type matcher doesn't derive from it. Reference documentation for the It.IsAnyType type matcher gives instructions on how to handle such cases:

If the generic type parameter has more specific constraints, you can define your own type matcher inheriting from the type to which the type parameter is constrained.

Hence, I created a customized version of the It.isSubType<T> type matcher which I wanted to use originally but couldn't:

[TypeMatcher]
public class IsFreshBasePageModel<T> : FreshBasePageModel, ITypeMatcher
  where T: FreshBasePageModel
{
  bool ITypeMatcher.Matches(Type typeArgument)
  {
    return typeof(T).IsAssignableFrom(typeArgument);
  }
}

The key difference between the two is that my type matcher derives from the FreshBasePageModel to satisfy the PushPageModel method's constraint. It applies the same constraint to its generic type argument so that the test fails at compile-time instead of at runtime if an invalid type is used (because of PushPageModel method's constraint the application code would fail to compile if the generic type argument didn't derive from FreshBasePageModel).

I could now use my new type matcher in the Setup method call:

coreMethodsMock
  .Setup(m => m.PushPageModel<IsFreshBasePageModel<SecondPageModel>>(
    It.Is<bool>(b => b)))
  .Returns(Task.CompletedTask)
  .Verifiable();

The compiler didn't complain anymore and the test verified the generic type argument as I wanted it to. Here's the complete final test code for reference:

[Test]
public void PushSecondPageCommandShouldPushSecondPage()
{
  var coreMethodsMock = new Mock<IPageModelCoreMethods>();

  coreMethodsMock
    .Setup(m => m.PushPageModel<IsFreshBasePageModel<SecondPageModel>>(
      It.Is<bool>(b => b)))
    .Returns(Task.CompletedTask)
    .Verifiable();

  var pageModel = new SamplePageModel();
  pageModel.CoreMethods = coreMethodsMock.Object;

  pageModel.PushSecondPageCommand.Execute(null);

  coreMethodsMock.VerifyAll();
}

A minimal sample project featuring this test is available on GitHub.

Moq's built-in type matchers (It.IsAnyType, It.IsValueType and It.IsSubtype<T>) can only be used when the mocked method's generic type arguments don't have any constraints. When the mocked methods have constraints, these type matchers will cause no implicit reference conversion errors because they don't satisfy the constraints. In such cases, custom type matchers that satisfy the constraints need to be implemented and used instead.


Constructing Immutable Objects with a Builder

$
0
0

Immutable objects can't change after they've been created. Because of this, all data needed for their initialization must be passed into them through the constructor. This can result in constructors with (too) many parameters. With the builder design pattern, this can be avoided.

In C#, read-only properties without setters are often used to hold data in immutable classes:

public class ImmutablePerson
{
  public string FirstName { get; }
  public string LastName { get; }
  public string? MiddleName { get; }
  public IEnumerable<string> ChildrenNames { get; }
}

These can only be set with an initializer or inside the constructor:

public ImmutablePerson(
  string firstName,
  string lastName,
  string? middleName = null,
  IEnumerable<string>? childrenNames = null)
{
  FirstName = firstName;
  LastName = lastName;
  MiddleName = middleName;
  ChildrenNames = childrenNames ?? new string[0];
}

The constructor for any reasonably sized class will end up having many parameters so that all the properties can be initialized. This quickly makes the calling code difficult to understand:

var immutable = new ImmutablePerson("John", "Doe", "Don", new[] { "Jane" });

Named parameters make this somewhat better but require developer discipline to consistently use them:

var immutable = new ImmutablePerson(
  firstName: "John",
  lastName: "Doe",
  middleName: "Don",
  childrenNames: new[] { "Jane" });

Of course, optional parameters for values with valid defaults can be omitted to make code shorter:

var immutable = new ImmutablePerson("John", "Doe");

The builder design pattern can be used to make this problem more manageable as the number of parameters grows:

Builder design pattern

To give the builder access to private members of the target class, it should be created as its inner class:

public class ImmutablePerson
{
  public class Builder
  {
  }
}

Now, the target class only needs a private constructor with the builder as its parameter. It can initialize its values by reading them from the builder:

private ImmutablePerson(Builder builder)
{
  FirstName = builder.FirstName;
  LastName = builder.LastName;
  MiddleName = builder.MiddleName;
  ChildrenNames = builder.ChildrenNames;
}

This means that the builder will have to expose these properties. They won't be immutable so they can have private setters:

public class Builder
{
  internal string FirstName { get; private set; }
  internal string LastName { get; private set; }
  internal string? MiddleName { get; private set; }
  internal IEnumerable<string> ChildrenNames { get; private set; } = new string[0];
}

The builder constructor still needs to initialize the properties without valid default values so that it's always in a valid state:

public Builder(string firstName, string lastName)
{
  FirstName = firstName;
  LastName = lastName;
}

For initializing the other properties, separate methods can be created. They return the builder instance to allow the more convenient fluent interface:

public Builder SetMiddleName(string middleName)
{
  MiddleName = middleName;
  return this;
}

public Builder SetChildrenNames(IEnumerable<string> childrenNames)
{
  ChildrenNames = childrenNames;
  return this;
}

The actual target class instance is created by calling the Build method which simply calls the previously implemented private constructor:

public ImmutablePerson Build()
{
  return new ImmutablePerson(this);
}

The calling code is more verbose but also much easier to follow:

var immutable = new ImmutablePerson.Builder("John", "Doe")
  .SetMiddleName("Don")
  .SetChildrenNames(new[] { "Jane" })
  .Build();

You can check the full code for the ImmutablePerson class and its Builder class on GitHub.

The builder pattern hides the complexities of creating a class. In the case of an immutable class, it can be used to avoid constructors with too many parameters. Since the builder is not immutable, the values can be set through multiple calls. The builder itself is then used as a source of values for the immutable class. Adding a fluent interface for the builder will allow the immutable class to still be created with only a single chain of calls.

String Literal Type Guard in TypeScript

$
0
0

String literal types are a lightweight alternative to string enums. With the introduction of the const assertions in TypeScript 3.4, even type guards can be implemented in a DRY manner.

Imagine the following type:

export interface FilterValue {
  key: FilterKey;
  value: string;
}

Where FilterKey is a string literal type:

export type FilterKey = 'name' | 'surname';

Keeping these in mind, let's implement a function that converts an object with key/value pairs (e.g. from a parsed query string) to a FilterValue array (skipping any unsupported keys):

export const FILTER_KEYS: FilterKey[] = ['name', 'surname'];

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (FILTER_KEYS.includes(key as FilterKey)) {
      filter.push({ key: key as FilterKey, value: queryParams[key] });
    }
  }
  return filter;
}

You can notice two issues with this code:

  • The list of allowed literal values is repeated in the FILTER_KEYS array.
  • Type safety is circumvented by explicitly casting the key variable to FilterKey when instantiating a FilterValue:
filter.push({ key: key as FilterKey, value: queryParams[key] });

The latter issue can be fixed with a type guard:

export function isFilterKey(key: string): key is FilterKey {
  return FILTER_KEYS.includes(key as FilterKey);
}

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (isFilterKey(key)) {
      filter.push({ key, value: queryParams[key] });
    }
  }
  return filter;
}

Now, typecasting isn't needed in the parseFilter function anymore. The compiler trusts the isFilter type guard that the key value is of FilterKey type. As long as it's implemented correctly, an invalid value can't be put into a FilterValue by incorrectly applying an explicit cast.

To avoid repeating the string literals in the FILTER_KEYS array, const assertion can be used:

export const FILTER_KEYS = ['name', 'surname'] as const;
export type FilterKey = typeof FILTER_KEYS[number];

The types FILTER_KEYS and FilterKeys remained identical to before. They are just defined differently.

Here's the full final code:

export const FILTER_KEYS = ['name', 'surname'] as const;
export type FilterKey = typeof FILTER_KEYS[number];

export interface FilterValue {
  key: FilterKey;
  value: string;
}

export function isFilterKey(key: string): key is FilterKey {
  return FILTER_KEYS.includes(key as FilterKey);
}

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (isFilterKey(key)) {
      filter.push({ key, value: queryParams[key] });
    }
  }
  return filter;
}

You can check the code at each step as separate commits in the corresponding repository on GitHub.

With TypeScript features such as const assertions and type guards, string literal types can be enhanced to provide even stronger type safety.

String Literal Type Guard in TypeScript

$
0
0

String literal types are a lightweight alternative to string enums. With the introduction of the const assertions in TypeScript 3.4, even type guards can be implemented in a DRY manner.

Imagine the following type:

export interface FilterValue {
  key: FilterKey;
  value: string;
}

Where FilterKey is a string literal type:

export type FilterKey = 'name' | 'surname';

Keeping these in mind, let's implement a function that converts an object with key/value pairs (e.g. from a parsed query string) to a FilterValue array (skipping any unsupported keys):

export const FILTER_KEYS: FilterKey[] = ['name', 'surname'];

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (FILTER_KEYS.includes(key as FilterKey)) {
      filter.push({ key: key as FilterKey, value: queryParams[key] });
    }
  }
  return filter;
}

You can notice two issues with this code:

  • The list of allowed literal values is repeated in the FILTER_KEYS array.
  • Type safety is circumvented by explicitly casting the key variable to FilterKey when instantiating a FilterValue:
filter.push({ key: key as FilterKey, value: queryParams[key] });

The latter issue can be fixed with a type guard:

export function isFilterKey(key: string): key is FilterKey {
  return FILTER_KEYS.includes(key as FilterKey);
}

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (isFilterKey(key)) {
      filter.push({ key, value: queryParams[key] });
    }
  }
  return filter;
}

Now, typecasting isn't needed in the parseFilter function anymore. The compiler trusts the isFilter type guard that the key value is of FilterKey type. As long as it's implemented correctly, an invalid value can't be put into a FilterValue by incorrectly applying an explicit cast.

To avoid repeating the string literals in the FILTER_KEYS array, const assertion can be used:

export const FILTER_KEYS = ['name', 'surname'] as const;
export type FilterKey = typeof FILTER_KEYS[number];

The types FILTER_KEYS and FilterKeys remained identical to before. They are just defined differently.

Here's the full final code:

export const FILTER_KEYS = ['name', 'surname'] as const;
export type FilterKey = typeof FILTER_KEYS[number];

export interface FilterValue {
  key: FilterKey;
  value: string;
}

export function isFilterKey(key: string): key is FilterKey {
  return FILTER_KEYS.includes(key as FilterKey);
}

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (isFilterKey(key)) {
      filter.push({ key, value: queryParams[key] });
    }
  }
  return filter;
}

You can check the code at each step as separate commits in the corresponding repository on GitHub.

With TypeScript features such as const assertions and type guards, string literal types can be enhanced to provide even stronger type safety.

Hooking into Vue Router from NuxtJS

$
0
0

The NuxtJS application framework for Vue.js replaces a lot of the low-level configuration through conventions, e.g. routing. But what if you need access to that configuration to implement a certain feature? For example, the vuex-router-sync module watches for route changes to sync the current route with the Vuex state. How could this be done in NuxtJS?

In NuxtJS, plugins provide a way to run custom code before the application is initialized. That's the right place for setting up global hooks. I didn't find it immediately obvious from the documentation, but the plugin receives the NuxtJS context as its parameter.

I must admit that even after figuring this out, I was still wondering how to get access to the router instance until I found an example on GitHub. The Vue router module makes itself available as a property of the Vue instance which is exposed as the app property of the NuxtJS context. This has allowed me to come up with the following simplified implementation of the vuex-router-sync module:

import { Context } from '@nuxt/types';
import { RootState } from '~/store';

export default ({ app: { store, router } }: Context) => {
  if (!(router && store)) {
    return;
  }

  router.afterEach((to, _from) => {
    store.commit('setFilter', to.query['filter']);
  });

  store.watch(
    (state: RootState) => state.filter,
    (filter: string) => {
      router.push({ query: { filter } });
    }
  );
};

The plugin hooks into the router and the store to synchronize a single value between the URL query string and the Vuex state. To enable it, just register it in nuxt.config.js:

export default {
  // ...
  plugins: [{ src: '~plugins/queryParamSync.ts' }],
  // ...
};

Of course, the code relies on the store configuration. Below is the relevant part, following the NuxtJS convention:

export interface RootState {
  filter: string;
}

export const state: () => RootState = () => ({
  filter: '',
});

export const mutations = {
  setFilter(state: RootState, value: string) {
    state.filter = value || '';
  },
};

As a page modifies the state, the URL query string will update automatically. The following computed property and method can take care of that:

import Vue from 'vue';

export default Vue.extend({
  computed: {
    filter() {
      return this.$store.state.filter;
    },
  },
  methods: {
    updateFilter(event: InputEvent) {
      this.$store.commit('setFilter', (event.target as HTMLInputElement).value);
    },
  },
});

Here's how they could be hooked up to the template, for example:

<div>
  <label for="filter">Filter</label>
  <input id="filter" type="text" :value="filter" @input="updateFilter" />
</div>

Full code for a working sample application is available in my GitHub repository.

In NuxtJS, plugins can be created to add custom application bootstrap code. The context parameter gives access to all NuxtJS internals, including its Vue instance. This makes it simple to integrate Vue.js modules and JavaScript libraries in general even if there's no NuxtJS plugin to download for that.

Vuex Modules in Multiple Files in NuxtJS

$
0
0

One of the options for creating a Vuex module in NuxtJS is to create separate state.ts, getters.ts, mutations.ts, and actions.ts files in the module folder. Especially for large modules, this can make the code easier to navigate. However, a very important detail about this approach is mentioned very briefly in the documentation.

I missed that detail and after I tried out my newly implemented Vuex module for the first time, I was greeted by the following error:

[vuex] getters should be function but "getters.getters" is {}.

It took me a while to figure out that the error was thrown because my export of getters didn't match what NuxtJS expected:

// wrong
export const getters = {
  isDefault: (state: RootState) => state.count === 0,
};

// correct
const getters = {
  isDefault: (state: RootState) => state.count === 0,
};
export default getters;

The important part is that the getters object has to be the default export from the file. Exports of the state, mutations, and actions from the other files have the same requirement. They must all be the default export from their corresponding file. Otherwise, you can expect errors similar to the one for getters:

store/state.ts should export a method that returns an object

[vuex] mutations should be function but "mutations.mutations" is {}.

[vuex] actions should be function or object with "handler" function but "actions.actions" is {}.

If you want to try it out for yourself, check the sample application from my GitHub repository. The latest commit features a working store and the one before that a broken one.

NuxtJS supports having separate files for the state, getters, mutations, and actions of a Vuex module. But they must all be default exports in the corresponding files.

Class Components with JSX in NuxtJS

$
0
0

Although both Vue.js and NuxtJS have TypeScript support, it often seems incomplete. For example, there's no compile-time type checking in Vue.js templates. Any errors will only be reported at runtime. Currently, the only way to achieve compile-time type safety is to use render functions with JSX syntax instead.

Vue.js Templates

The component inputs (props in Vue.js terminology) can be formally described to some extent, but there's no way to specify the type of an object or the signature of a function. The following component uses Vue Property Decorator syntax for that:

@Component
export default class VueTextInput extends Vue {
  @Prop({ type: String, required: true }) readonly id!: string;
  @Prop({ type: String, required: true }) readonly label!: string;
  @Prop({ type: String, required: false, default: '' }) readonly value!: string;

  @Emit()
  private valueChanged(_newValue: string) {}

  private handleInput(event: InputEvent): void {
    this.valueChanged((event.target as HTMLInputElement).value);
  }
}

Still, the build will succeed if you omit a required prop because of a typo, for example. Only at runtime when the component is rendered, a warning will be printed to the browser console if you're using a development build:

[Vue warn]: Missing required prop: "id"

found in

---> <VueTextInput> at components/VueTextInput.vue
       <VuePage> at pages/vue.vue
         <Nuxt>
           <Layouts/default.vue> at layouts/default.vue
             <Root>

If you make a typo in the custom event name instead, you won't even get a warning in the browser console. Your event handler simply won't get called.

Hunting down these errors can be very time-consuming in a large application. If you for some reason change the props of a component when it's already used in many places, you will need to find and fix all these occurrences without any help of the compiler or other tools.

Similarly, if you make a typo in the name of a property that you want to bind to a prop, only a warning will be printed to the browser console in the development build:

[Vue warn]: Property or method "Id" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

found in

---> <VuePage> at pages/vue.vue
       <Nuxt>
         <Layouts/default.vue> at layouts/default.vue
           <Root>

If you're using Vetur for Visual Studio Code, you can at least enable its experimental Template Interpolation Service feature:

{
  "vetur.experimental.templateInterpolationService": true
}

This will allow you to see the latter type of errors marked as such in the editor:

Error reported by Vetur's Template Interpolation Service

VTI and vue-type-check are command-line tools with similar functionality that can be included in the build process but I couldn't get either to work on my Windows machine.

Render Functions with JSX

You can achieve a much higher level of compile-time type safety if you decide to replace the templates with the render function and the JSX syntax. This approach, as described below, is highly inspired by Greg Solo's blog post which I stumbled upon while looking for a way to improve the TypeScript development experience with Vue.js.

An equivalent component to the one above would be implemented as follows:

export interface TsxTextInputProps {
  id: string;
  label: string;
  value?: string;
  valueChanged?: (value: string) => void;
}

@Component({
  props: {
    id: { type: String, required: true },
    label: { type: String, required: true },
    value: { type: String, required: false, default: '' },
    valueChanged: { type: Function, required: false },
  },
  render(this: TsxTextInput): Vue.VNode {
    return (
      <div>
        <label for={this.$props.id}>{this.$props.label}</label>
        <input
          id={this.$props.id}
          name={this.$props.id}
          type="text"
          value={this.$props.value}
          onInput={(event: InputEvent) => this.handleInput(event)}
        />
      </div>
    );
  },
})
export default class TsxTextInput extends VueComponent<TsxTextInputProps> {
  private handleInput(event: InputEvent): void {
    this.$props.valueChanged?.((event.target as HTMLInputElement).value);
  }
}

For this to work, the generic VueComponent class must also be added to the project:

export class VueComponent<P> extends Vue {
  $props!: P;
}

The following compiler option is required in tsconfig.json to enable JSX support in TypeScript:

{
  "compilerOptions": {
    "jsx": "preserve"
  }
}

To correctly handle the JSX types in Vue.js, the following shim must be added to a .d.ts file in the project:

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode {}
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
    interface ElementAttributesProperty {
      $props: {};
    }
  }
}

Unfortunately, the props definitions are now duplicated. The interface is used by the TypeScript compiler to ensure type safety. And the standard props declaration is still required by Vue.js. You can also notice that I replaced the custom event with a callback prop. Unlike custom events, these are also type-checked at compile time.

At the price of a few more lines of code, the mistakes as in the examples above will now be detected at compile-time (and also directly in the code editor):

  • a mistyped prop name:

    Property 'Id' does not exist on type 'TsxTextInputProps'.

  • a mistyped callback prop name (as a replacement for a custom event):

    Property 'valueChange' does not exist on type 'TsxTextInputProps'.

  • a mistyped property name bound to a prop:

    Cannot find name 'Id'.

It's also worth mentioning that I found no way to make Vue.js Scoped CSS working with this approach. However, if you need component-scoped CSS in your application, you can still use CSS Modules.

In this case, you will need an accompanying .vue file for each component in which you will put the CSS:

<script src="./TsxTextInput.tsx"></script>

<style module>
  .input-label {
    color: red;
  }
</style>

The styles will be available to you in the $style property of the component:

<label class={this.$style['input-label']} for={this.$props.id}>
  {this.$props.label}
</label>

To make the TypeScript compiler aware of it, another shim is required in the project:

declare module 'vue/types/vue' {
  interface Vue {
    $style: { [key: string]: string };
  }
}

You can get a full working sample with Vue.js template-based components and JSX-based components in my repository on GitHub.

Although TypeScript support in Vue.js seems to be an afterthought, you can still get better compile-time checking in templates if you decide to use render functions with JSX instead of the more commonly used Vue.js templates.

There are downsides to it, though. Although JSX is officially supported in Vue.js, it won't necessarily work out-of-the-box in all frameworks and boilerplates. Additional configuration and workarounds will be required for that. And you'll find less resources online when searching for solutions. It's up to you to decide if you're willing to pay that price.

Let's all hope that TypeScript support will be better in Vue.js 3.

Testing JSX components with Jest in NuxtJS

$
0
0

Even if you select TypeScript and Jest support when creating a new NuxtJS project, it still isn't fully configured for writing tests in TypeScript, let alone for testing components written with JSX syntax. This post describes my journey to a working configuration.

As soon as I've written my first describe call in a test, Visual Studio Code already reported an error:

Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try npm i @types/jest or npm i @types/mocha and then add jest or mocha to the types field in your tsconfig.

Thanks to the detailed error message, fixing this problem wasn't difficult. I installed the type definitions for Jest:

npm i -D @types/jest

And added them to tsconfig.json:

{
  "compilerOptions": {
    "types": ["@types/node", "@nuxt/types", "@types/jest"]
  }
}

I could now write my first test without any errors reported by Visual Studio Code:

import { shallowMount } from '@vue/test-utils';
import TsxTextInput, { TsxTextInputProps } from '../TsxTextInput';

describe('TsxTextInput component', () => {
  it('should render the input label', () => {
    const props: TsxTextInputProps = {
      id: 'id',
      label: 'Label:',
    };

    const wrapper = shallowMount(TsxTextInput, { propsData: props });

    expect(wrapper.find('label').text()).toBe(props.label);
  });
});

However, they failed to run:

Cannot find module '../TsxTextInput' from 'tsxTextInput.spec.ts'

However, Jest was able to find:
    '../TsxTextInput.tsx'

You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['ts', 'js', 'vue', 'json'].

Again, the error message was quite helpful. I had to add the .tsx file extension to jest.config.js:

module.exports = {
  //...
  moduleFileExtensions: ['ts', 'tsx', 'js', 'vue', 'json'],
  //...
};

This only caused a different error to be returned instead:

SyntaxError: Cannot use import statement outside a module

This one didn't include any helpful guidance. Still, it wasn't too difficult to figure out that the syntax was invalid because it was treated as pure JavaScript. I modified jest.config.js to have the .tsx files processed by ts-jest.

module.exports = {
  //...
  transform: {
    '^.+\\.tsx?: 'ts-jest',
    '^.+\\.js: 'babel-jest',
    '.*\\.(vue): 'vue-jest',
  },
  //...
};

And once again, this only resulted in a different error:

SyntaxError: Unexpected token '<'

For this one, I couldn't find a quick solution myself. In the end, I consulted a working example by Gregory Soloschenko. I had to reference two presets in my configuration files:

  • For Jest, I added the following at the top of jest.config.js:

    module.exports = {
      preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
      //...
    };
    
  • For Babel, I had to add @vue/cli-plugin-babel/preset. However, it didn't seem to work in .babelrc. Therefore, I changed the file to babel.config.js and ended up with the following:

    module.exports = {
      env: {
        test: {
          presets: [
            ['@vue/cli-plugin-babel/preset'],
            [
              '@babel/preset-env',
              {
                targets: {
                  node: 'current',
                },
              },
            ],
          ],
        },
      },
    };
    

Of course, I also had to install the corresponding NPM packages:

npm i -D @vue/cli-plugin-unit-jest
npm i -D @vue/cli-plugin-babel

Now, the tests passed. But there were still warnings because of version conflicts:

ts-jest[versions](WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.

ts-jest[versions](WARN) Version 24.9.0 of babel-jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.

To resolve them, I had to uninstall packages that were indirectly included in different versions. This put my node_modules folder in an inconsistent state and forced me to reinstall @vue/cli-plugin-unit-jest:

npm uninstall -D jest
npm uninstall -D ts-jest
npm uninstall -D vue-jest
npm i -D @vue/cli-plugin-unit-jest

Finally, the test run completed without any errors or warnings. To include my JSX components in the coverage report, a made one last modification to jest.config.js:

module.exports = {
  //...
  collectCoverageFrom: [
    '<rootDir>/components/**/*.{vue,tsx}',
    '<rootDir>/pages/**/*.{vue,tsx}',
  ],
  //...
};

If you have problems following my steps, I suggest you take a look at my repository on GitHub with a fully configured example.

Initial NuxtJS configuration for writing Jest tests in TypeScript for JSX components is incomplete. The guidance in error messages takes you only so far. To get everything working, it's best to rely on presets provided by the Vue CLI tooling.


Using InversifyJS in NuxtJS

$
0
0

Unlike Angular, Vue.js doesn't have a built-in dependency injection framework and neither has NuxtJS. There's a way to inject members into components with Vue.js and NuxtJS plugins but that's just a small subset of what a real dependency injection framework can do. Of course, nothing is stopping you from using one in your Vue.js or NuxtJS application. InversifyJS is a popular choice in the JavaScript ecosystem.

The installation procedure will be the same as in any other JavaScript application:

  • Install the required NPM packages:

    npm i inversify reflect-metadata
    
  • Update the tsconfig.json file by setting the emitDecoratorMetadata and adding reflect-metadata to the types array (or setting typeRoots instead):

    {
      "compilerOptions": {
        "target": "es2018",
        "module": "esnext",
        "moduleResolution": "node",
        "lib": ["esnext", "esnext.asynciterable", "dom"],
        "esModuleInterop": true,
        "allowJs": true,
        "sourceMap": true,
        "strict": true,
        "noEmit": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "baseUrl": ".",
        "paths": {
          "~/*": ["./*"],
          "@/*": ["./*"]
        },
        "typeRoots": ["node_modules/@types"]
      },
      "exclude": ["node_modules", ".nuxt", "dist"]
    }
    

For each dependency you will have to define a type (or use an existing one as I do below for the NuxtAxiosInstance):

export interface GithubRepository {
  getOrgRepos(org: string): Promise<Repo[]>;
}

And implement it, of course:

import { inject, injectable } from 'inversify';
import { NuxtAxiosInstance } from '@nuxtjs/axios';

@injectable()
export class AxiosGithubRepository implements GithubRepository {
  constructor(
    @inject(SYMBOLS.NuxtAxiosInstance) private axios: NuxtAxiosInstance
  ) {}

  async getOrgRepos(org: string): Promise<Repo[]> {
    return await this.axios.$get<Repo[]>(
      `https://api.github.com/orgs/${org}/repos`
    );
  }
}

The implementations must be decorated with the @injectable decorator.

Constructor injection can be used for injecting dependencies. Each parameter must be decorated with the @inject decorator. Its parameter is a type identifier that must be defined for each dependency:

export const SYMBOLS = {
  GithubRepository: Symbol('GithubRepository'),
  NuxtAxiosInstance: Symbol('NuxtAxiosInstance'),
};

The same identifiers are used when configuring the dependency injection container:

import 'reflect-metadata';
import { Container } from 'inversify';

export const container = new Container();
container
  .bind<GithubRepository>(SYMBOLS.GithubRepository)
  .to(AxiosGithubRepository)
  .inSingletonScope();

I used singleton scope in the snippet above but other options are supported as well.

The import for reflect-metadata is really important. It must be done exactly once in your application. The file with container configuration seems a good place for it. If you forget to do it, the application will fail at runtime with the following error:

Reflect.hasOwnMetadata is not a function

You might have noticed that I haven't configured the binding for the NuxtAxiosInstance. That's because its instance can't simply be created. The NuxtJS Axios module takes care of its initialization and makes it available in the NuxtJS context. Such dependencies will need to be bound in a NuxtJS plugin which ensures that the code is run before the Vue.js application is started (and after the modules are initialized):

import { Context } from '@nuxt/types';
import { NuxtAxiosInstance } from '@nuxtjs/axios';

export default ({ $axios }: Context) => {
  container
    .bind<NuxtAxiosInstance>(SYMBOLS.NuxtAxiosInstance)
    .toConstantValue($axios);
};

As you can see, I get the instance from the NuxtJs context and bind it as a constant value for its type.

The plugin must be registered in nuxt.config.js to be run:

export default {
  // ...
  plugins: ['plugins/bindInversifyDependencies.ts'],
  // ...
};

The constructor injection will only work when InversifyJS is creating an instance with the dependency. If that's not the case, lazy property injection can be used instead. This requires some additional setup:

  • NPM package installation:

    npm i inversify-inject-decorators
    
  • Initialization of the @lazyInject decorator (best done in the same file as the InversifyJS container configuration):

    export const { lazyInject } = getDecorators(container);
    

To take advantage of that in components (and NuxtJS pages), the class-style syntax must be used:

@Component
export default class IndexPage extends Vue {
  repos: Repo[] = [];
  org: string = '';

  @lazyInject(SYMBOLS.GithubRepository)
  private githubRepository!: GithubRepository;

  async refresh(): Promise<void> {
    this.repos = await this.githubRepository.getOrgRepos(this.org);
  }
}

Unfortunately, sometimes even this isn't possible. For example, the NuxtJS asyncData method is invoked before the component is initialized. In this case, the only option is to request the dependency instance directly from the container:

@Component({
  asyncData: async (_context: Context) => {
    const githubRepository = container.get<GithubRepository>(
      SYMBOLS.GithubRepository
    );
    const repos = await githubRepository.getOrgRepos('damirscorner');
    return { repos };
  },
})
export default class IndexPage extends Vue {}

This covers all the different use cases I've encountered in NuxtJS so far. However, in development mode the application will still fail with the following error when using SSR (server-side rendering):

Cannot read property 'constructor' of null

To resolve it, the following configuration must be added to the nuxt.config.js file:

export default {
  // ...
  render: {
    bundleRenderer: {
      runInNewContext: false,
    },
  },
  // ...
};

Source code for a working NuxtJS sample application using all of the above is available in my GitHub repository.

Since Vue.js and NuxtJS don't have a built-in dependency injection framework, InversifyJS can be added to your project if you would like to take advantage of this pattern. To use it fully, you will need some understanding of the NuxtJS internals. This post should help you set up everything initially and learn the patterns to use in different scenarios.

Strongly-typed Vuex Store in NuxtJS

$
0
0

Vuex store code can be quite verbose, especially with wrappers for type-safety in TypeScript. A lot of that plumbing can be avoided with the vuex-module-decorators package. There's some extra configuration required to get it working in NuxtJS.

The core module definition for NuxtJS is the same as in plain Vue.js and is well documented:

  • The store module is defined as a class extending VuexModule with @Module decorator.

    @Module
    export default class SampleModule extends VuexModule {
      // ...
    }
    
  • The state consists of class properties.

    count = 0;
    
  • Getters are implemented as getter functions.

    get isDefault(): boolean {
      return this.count === 0
    }
    
  • Mutations are methods with the @Mutation decorator. Only one parameter is supported. For more, they should be passed as a single payload object. Methods must be synchronous and shouldn't return a value

    @Mutation
    increment(): void {
      this.count++
    }
    
  • Actions are methods with the @Action decorator. They also support only a single parameter like mutations. They can be asynchronous and should return a promise. The rawError parameter should be set if the method is supposed to throw errors so that those errors aren't wrapped.

    @Action({ rawError: true })
    incrementAsync(): Promise<void> {
      return new Promise<void>((resolve) => {
        setTimeout(() => {
          this.increment()
          resolve()
        }, 100)
      })
    }
    

To use the module from NuxtJS it should be namespaced and dynamic. This can be configured with @Module decorator parameters:

@Module({ name: 'sample', stateFactory: true, namespaced: true })
export default class SampleModule extends VuexModule {
  // ...
}

When using NuxtJS in SSR mode, the suggested pattern in the vuex-module-decorators documentation shouldn't be implemented because it introduces singleton store variables which will cause the state to be shared between requests. Instead, the guidance for SSR in the same documentation should be followed.

The following helper function can make that code a bit simpler:

export function getSampleModule(store: Store<any>): SampleModule {
  return getModule(SampleModule, store);
}

The exported function can then be used instead of the plain getModule function to access the store from elsewhere in the application, e.g. a page or a component. All module members (state, getters, mutations, and actions) are fully typed:

import Vue from 'vue';
import Component from 'vue-class-component';
import { getSampleModule } from '~/store';

@Component
export default class IndexPage extends Vue {
  get count(): number {
    return getSampleModule(this.$store).count;
  }

  get isDefault(): boolean {
    return getSampleModule(this.$store).isDefault;
  }

  increment() {
    getSampleModule(this.$store).increment();
  }

  incrementAsync() {
    getSampleModule(this.$store).incrementAsync();
  }
}

A full working example is in my GitHub repository.

The vuex-module-decorators package is a great choice when using TypeScript as it allows strongly-typed access to the Vuex store with minimum additional plumbing code. When defined and used correctly, such modules can also be used with NuxtJS, even in SSR mode.

Detect Buggy Property Assignment Early

$
0
0

I was recently invited to look at a piece of seemingly working JavaScript code. The problem was that the TypeScript compiler was complaining about it after it was annotated with type information. In the end, these errors uncovered a hidden bug in the code.

The code was a React onChange handler:

handleInputChange = (event) => {
  const target = event.target;
  const name = target.name;
  const value = name === 'visible' ? target.checked : target.value;
  var data = this.state.data;
  data[name] = value;
  this.setState({
    data,
  });
};

Adding type information wasn't a big deal:

interface Entry {
  id: string;
  description: string;
  minPrice: number;
  maxPrice: number;
  visible: boolean;
}

export class FakeComponent {
  state: { data: Entry };

  // ...

  handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    // ...
  };
}

However, the compiler complained about the following line of code

data[name] = value;

The error?

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Entry'.

No index signature with a parameter of type 'string' was found on type 'Entry'.

Sure, the problem can be avoided by disabling type checking for this particular assignment:

var data = this.state.data as any;

Although the resulting JavaScript would be identical to the original code, this defeats the purpose of using TypeScript in the first place. We want to take advantage of type checking, not avoid it.

The reported error is caused by the fact that name is of type string but only some of its values are valid field names of Entry. This can be resolved with a type guard:

if (
  name === 'id' ||
  name === 'description' ||
  name === 'minPrice' ||
  name === 'maxPrice' ||
  name === 'visible'
) {
  data[name] = value;
}

This would make sure that no property assignment happens if the value of name doesn't match an Entry field name. TypeScript protects us from adding extra properties to the object. Yes, there might be no harm in doing that and we might also know that the names of input elements match the field names, but the TypeScript compiler doesn't know that and the above type guard is the only way to be sure.

Even with this change, the TypeScript compiler still complains:

Type 'string | boolean' is not assignable to type 'never'.

Type 'string' is not assignable to type 'never'.

The problem is that Entry fields are of different types and the code still doesn't ensure that the assigned value matches the type of the underlying field. To fix the error, we must make sure that the types of values match:

if (name === 'visible') {
  data[name] = target.checked;
} else if (name === 'id' || name === 'description') {
  data[name] = target.value;
} else if (name === 'minPrice' || name === 'maxPrice') {
  data[name] = parseInt(target.value, 10);
}

With this change, there are no more TypeScript errors. The changes also propagated to the generated JavaScript:

this.handleInputChange = (event) => {
  const target = event.target;
  const name = target.name;
  var data = this.state.data;
  if (name === 'visible') {
    data[name] = target.checked;
  } else if (name === 'id' || name === 'description') {
    data[name] = target.value;
  } else if (name === 'minPrice' || name === 'maxPrice') {
    data[name] = parseInt(target.value, 10);
  }
  this.setState({
    data,
  });
};

In the process of fixing the TypeScript errors, we also fixed a bug that caused a string to be assigned to minPrice and maxPrice instead of a parsed number. Thanks to JavaScript type coercion this could manifest in a nasty hidden bug:

const component = new FakeComponent({
  id: '1',
  description: 'initial description',
  minPrice: 10,
  maxPrice: 20,
  visible: true,
  photos: [],
});

const event = {
  target: {
    name: 'minPrice',
    value: '12',
  },
} as React.ChangeEvent<HTMLInputElement>;

component.handleInputChange(event);
const entry = component.state.data;
const midPrice = entry.minPrice + entry.maxPrice;

expect(midPrice).toBe(16); // Received: 610

You can check the full code for the above example in my GitHub repository.

Although TypeScript sometimes seems to complain about working JavaScript code, it's always because of a type mismatch of some kind. It might be as innocent as a precondition not expressed in TypeScript code (what input fields are available in our example). But it might also be a real bug that you'd struggle to find otherwise (having a string value where a number is expected in our example). It's usually worth it to get to the bottom of each error instead of simply opting out of type checking.

Configuring Storybook for Vue with TypeScript

$
0
0

Storybook is a great tool for component development. But although it supports many frameworks, it still sometimes gives the appearance of being React-first. For example, the default configuration for Vue.js doesn't have TypeScript support.

The init command recognizes a Vue.js project and sets everything up:

npx -p @storybook/cli sb init

You could try writing a story in TypeScript:

export default {
  title: "components/Button",
};

export const Demo = () => ({
  components: { Button },
  template: `<Button text="Click me!"></Button>`,
});

However, Storybook will simply ignore it. To fix that, you first need to change the pattern in .storybook/main.js which only includes JavaScript files by default:

module.exports = {
  stories: ["../src/**/*.stories.ts"],
  // ...
};

Now, the TypeScript file will be processed, but will immediately fail with an error:

Module parse failed: Unexpected character '@' (10:0)
File was processed with these loaders:
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.

The error message is very informative. The problem can be fixed by customizing Storybook's Webpack configuration with Vue TypeScript loader:

module.exports = {
  // ...
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.ts$/,
      loader: "ts-loader",
      options: { appendTsSuffixTo: [/\.vue$/] },
    });
    return config;
  },
};

This will fix the problem. But there's another issue with Storybook's TypeScript configuration. A new Vue.js project includes a path alias configuration:

"paths": {
  "@/*": [
    "src/*"
  ]
},

Storybook will not yet recognize such paths:

ERROR in ./src/components/Button.stories.ts
Module not found: Error: Can't resolve '@/components/Button.vue

There's a Webpack plugin to help with that:

npm i -D tsconfig-paths-webpack-plugin

Of course, it must be included in Storybook's Webpack configuration:

const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");

module.exports = {
  // ...
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.ts$/,
      loader: "ts-loader",
      options: { appendTsSuffixTo: [/\.vue$/] },
    });
    config.resolve.plugins = config.resolve.plugins || [];
    config.resolve.plugins.push(new TsconfigPathsPlugin({}));
    return config;
  },
};

For reference, you can check the full configuration in my repository on GitHub.

Although Storybook's default configuration for Vue.js doesn't include support for TypeScript, it can be added by customizing its Webpack configuration with the well-documented missing TypeScript bits.

Creating a Vue.js Component Library

$
0
0

Although Vue CLI has built-in support for building component libraries, there's still some work in creating one, especially if you want it to be TypeScript and SSR compatible.

To specify what gets included in the library, an entry file should be created which exports each component like that:

export { default as Button } from "./components/Button.vue";

The file is then referenced from the build script:

{
  "scripts": {
    "build-lib": "vue-cli-service build --target lib src/index.ts"
  }
}

The command puts the generated files in the dist folder. To include them in the NPM package, they must be declared in the package.json file:

{
  "files": ["dist/*"],
  "main": "./dist/vue-component.umd.js"
}

For testing purposes, you can create a local NPM package with an extended build script:

{
  "scripts": {
    "build-lib": "vue-cli-service build --target lib src/index.ts && npm pack && shx mv ./vue-component-0.1.0.tgz .."
  }
}

I'm using shx for moving the generated package file.

The package can then be installed in another Vue.js project:

npm i ../vue-component-0.1.0.tgz

That's enough to import the components in a .vue file:

import { Button } from "vue-component";

In a JavaScript file, this will just work. But the TypeScript compiler will complain:

Could not find a declaration file for module 'vue-component'.

This can be fixed by creating a placeholder type declaration file vue-component/index.d.ts somewhere in the project, e.g. in the types folder:

declare module "vue-component";

The file must be referenced in tsconfig.json, either by adding the root folder to the typeRoots setting:

{
  "typeRoots": ["./node_modules/@types", "types"]
}

Or by adding the folder itself to the types setting:

{
  "types": ["@types/node", "@nuxt/types", "types/vue-component"]
}

However, this will only prevent the error. It won't provide the types.

A better solution is to include the type definitions in the component library. This can be done in a types/index.d.ts file:

import { VueConstructor } from "vue";

export const Button: VueConstructor;

It's enough to just declare them as components, as Vue.js can't take advantage of more type information. Other Vue.js component libraries do that as well.

The file must also be declared in package.json:

{
  "files": ["dist/*", "types/*"],
  "types": "./types/index.d.ts"
}

With this change in the component library, the TypeScript error will go away even without the placeholder types in the application code.

The component library includes the styles in a separate CSS file. This means that it must be included globally in the consuming project. In NuxtJS, this can be done in nuxt.config.js:

export default {
  // ...
  css: ["node_modules/vue-component/dist/vue-component.css"],
  // ...
};

Component libraries typically solve this with a module.

Vue CLI allows you to inline the styles in the generated JavaScript file by including a vue.config.js file in the component project:

module.exports = {
  css: {
    extract: false,
  },
};

However, this approach doesn't work with server-side rendering (SSR). The project will still build, but will fail at runtime:

document is not defined

You can find a working sample in my repository on GitHub. It consists of a component library and a NuxtJS application using it.

Even when using Vue CLI to create a component library, you still need to manually edit the package.json file to configure and generate the NPM package. To make the components work with TypeScript, you should also include the type definitions in the package.

Viewing all 484 articles
Browse latest View live