Skip to content

Testing Web Components with Karma and TypeScript

Imagine yourself building an electric car.

By design, this car has an electric motor, a cooling system, a battery, a charge port, a transmission, and other units. Every part of the system has requirements that must be met before being attached to the other components.

If any of these components has not been properly tested, the car may not work.

Similarly, software can be considered as a collection of units of functionality. The quality of the entire system can be determined by the quality of every part of which its made.

Unit Tests

Unit tests are functions implemented to verify the behavior of the code under certain scenarios. Let's suppose we have a function that returns a greeting:

function greeting(name?: string): string {
  return name? `Hello ${name}!`: 'Hello!';
}

Following the TDD approach, you can write your first test with a valid name='Luis' as an input with Hello Luis! expected as a result.

However, how could you ensure the correct operation of this function under other parameters? Let's think in the following cases:

  • An invalid situation, where the function receives an empty string or undefined.
  • A valid situation, where the function receives a valid name as a string.

Both conditions can be considered part of the same scenario. Then, you may define a formal way to write them as:

  - Scenario: "Greeting Function"
    - assert(greeting() == 'Hello!')
    - assert(greeting('Luis') == 'Hello Luis!')

In the JavaScript world, there are a lot of options in terms of tools to implement this kind of test. Let's talk about them in the next section.

Testing Tools for Web Components

Think about the steps you need to perform before you start running unit tests in your JavaScript/TypeScript project.

First, you probably have to think about installing and configuring some tools:

  • A Testing Framework, which defines a set of guidelines and rules to implement your test cases.

  • A Test Runner, which is the tool that executes your unit tests as a whole suite.

Not only that, it would be great to consider a Web Test Runner to run the JavaScript code and "render" the DOM elements. This is particularly important when testing our web components.

With so many options in mind, it would be easy to lose your way and run out of time. Thus, the open-wc team provides a set of tools and recommendations to facilitate testing tasks.

Setup

You can use the project scaffolding tool to create a new project from scratch. The TypeScript support and common tooling for tests is only one command away:

npm init @open-wc
# Select "Scaffold a new project" (What would you like to do today?)
# Select "Application" (What would you like to scaffold?)
# Mark/Select "Linting", "Testing", "Demoing" and "Building" (What would you like to add?)
# Yes (Would you like to use TypeScript?)
# Mark/Select "Testing", "Demoing" and "Building" (Would you like to scaffold examples files for?)
# my-project (What is the tag name of your application/web component?)
# Yes (Do you want to write this file structure to disk?)
# Yes, with npm (Do you want to install dependencies?)

Next, pay attention to the generated files:

|- my-project/
    |- src/
    |- test/
        |- my-project.test.ts
    |- karma.conf.js
    |- package.json
    |- tsconfig.json
    |... other files/folders

As you can see, there is a test folder where we can add our testing files. Just make sure to use the .test.ts extension for them.

The karma.conf.js file contains the configuration needed for testing with Karma. Why you should use Karma? In the words of the open-wc team:

We recommend karma as a general-purpose tool for testing code that runs in the browser. Karma can run a large range of browsers, including IE11. This way, you are confident that your code runs correctly in all supported environments.

Also, the package.json file defines the following scripts for testing:

{
  "scripts": {
    ...
    "test": "tsc && karma start --coverage",
    "test:watch": "concurrently --kill-others --names tsc,karma \"npm run tsc:watch\" \"karma start --auto-watch=true --single-run=false\"",
  },
  ...
}

Use the npm run test command for single running, and use npm run test:watch for running unit tests in watch mode(Enable watching files and executing the tests whenever one of these file changes).

Testing Libraries

The open-wc team recommends the following libraries for testing:

  • mocha, as the testing framework running on Node.js and in the browser.

  • chai, as the assertion library. It can be used with any other JavaScript framework today.

Along with them, there are a set of tools and helpers available:

All these powerful tools are configured and available as a single package: @open-wc/testing. You don't need to perform an additional step to start writing your tests!

Writing Unit Tests

If you're following the Web Components with TypeScript series in the blog, you'll find a fully functional project with Web Components implemented using LitElement and TypeScript.

Testing the About Page

This would be an example of a basic Web Component implementation:

import { LitElement, html, customElement } from 'lit-element';

@customElement('lit-about')
export class About extends LitElement {
  render() {
    return html`
      <h2>About Me</h2>
      <p>
        Suspendisse mollis lobortis lacus, et venenatis nibh sagittis ac.
        ...
      </p>
    `;
  }
}

Let's create a file, /test/about.test.ts, for its unit tests with the initial content:

import { LitElement, html, customElement, css, property } from 'lit-element';
import { About } from '../src/about/about';

describe('About Component', () => {
  let element: About;

  beforeEach(async () => {
    element = await fixture<About>(html` <lit-about></lit-about> `);
  });
});

Here's the sequence of actions for clarity:

  • describe('About Component') gives a meaningful description of the testing scenario.

  • let element: About declares a variable to contain a reference to the rendered Web Component. This variable will be available for the entire describe block.

  • beforeEach() function runs as a set of preconditions before running the tests. In this case, it's rendering the string provided as <lit-about></lit-about> and puts it in the DOM. This is an asynchronous operation, and will return a Promise<About>

  • Finally, since we're using async/await here, the variable element will contain the expected About component.

At this point, we're ready to start with the component testing. Add the following test right after beforeEach:

  it('is defined', () => {
    assert.instanceOf(element, About);
  });

This test asserts that the element variable contains an instance of the About class.

Next, add the following it block to test the title rendering:

  it('renders \'About Me\' as a title', () => {
    const h2 = element.shadowRoot!.querySelector('h2')!;
    expect(h2).to.exist;
    expect(h2.textContent).to.equal('About Me');
  });

Here's the sequence of actions explained:

  • it() function sets a test with a meaningful description for its purpose.

  • element.shadowRoot!.querySelector('h2')! looks for a DOM node that matches the specified selector "h2". That element should be part of the Shadow DOM. Then, it's required to do it through element.shadowRoot call.

  • The previous call can produce null as a result (in case there's no match with the selector). In TypeScript, we can use the non-null assertion operator(!). In other words, you're telling the TypeScript compiler: "I'm pretty sure the element exist".

  • expect(h2).to.exist is the formal assertion to verify that h2 variable is not null or undefined.

  • Next, there is an assertion about the text content for the provided selector h2.

You can apply the same principles to validate the rendered paragraph:

  it('renders a paragraph', () => {
    const paragraph = element.shadowRoot!.querySelector('p')!;
    expect(paragraph).to.exist;
    expect(paragraph.textContent).to.contain(
      'Suspendisse mollis lobortis lacus'
    );
  });

Testing the Blog Card Component

As you may remember, the Blog Card Component implementation is using LitElement too:

import { LitElement, html, customElement, css, property } from 'lit-element';
import { Post } from './post';

@customElement('blog-card')
export class BlogCard extends LitElement {
  static styles = css`
    // your styles goes here
  `;

  @property({ type: Object }) post?: Post;

  render() {
    return html`
      <div class="blog-card">
        <div class="blog-description">
          <h1>${this.post?.title}</h1>
          <h2>${this.post?.author}</h2>
          <p>${this.post?.description}</p>
          <p class="blog-footer">
            <a class="blog-link" @click="${this.handleClick}">Read More</a>
          </p>
        </div>
      </div>
    `;
  }

  public handleClick() {
    this.dispatchEvent(
      new CustomEvent('readMore', { detail: this.post })
    );
  }
}

Let’s create another file for the unit tests as /test/blog-card.test.ts with the initial content:

import { assert, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { BlogCard } from '../src/blog/blog-card';
import { Post } from '../src/blog/post';

const post: Post = {
  id: 0,
  title: 'Web Components Introduction',
  author: 'Luis Aviles',
  description: 'A brief description of the article...',
};

describe('Blog Card Component', () => {
  let element: BlogCard;

  beforeEach(async () => {
    element = await fixture<BlogCard>(html`<blog-card .post="${post}"></blog-card> `);
  });
});

There's a lot in common with the previous test file. However, in this case, it's needed to set a Post object to be sent as a parameter to create the <blog-card> component.

Let's add some tests right after beforeEach function:

  it('is defined', () => {
    assert.instanceOf(element, BlogCard);
  });

  it('defines a post attribute', () => {
    expect(element.post).to.equal(post);
  });

The first test is about an instance assertion: Is this 'element' an instance of BlogCard class?.

The second test goes a little bit deeper since it's verifying the value of the custom element property post. Remember that property was defined using the @property decorator.

Now, let's add some tests to verify the rendering of the different sections from our Web component:

  it('renders \'Web Components Introduction\' as a title', () => {
    const h1 = element.shadowRoot?.querySelector('h1')!;
    expect(h1).to.exist;
    expect(h1.textContent).to.equal('Web Components Introduction');
  });

  it('renders \'Luis Aviles\' as author', () => {
    const h2 = element.shadowRoot?.querySelector('h2')!;
    expect(h2).to.exist;
    expect(h2.textContent).to.equal('Luis Aviles');
  });

  it('renders a \'Read More\' link', () => {
    const a = element.shadowRoot?.querySelector('a')!;
    expect(a).to.exist;
    expect(a.getAttribute('class')).to.equal('blog-link');
    expect(a.textContent).to.equal('Read More');
  });

There's something new here: expect(a.getAttribute('class')). The getAttribute function will inspect the DOM and it will look for the class attribute. On the template side, it defines the link to Read More about the current blog post: <a class="blog-link">Read More</a>

Since <blog-card> is a reusable component, it would be useful to test the event propagation once it's selected:

  it('dispatch a click event', async () => {
    setTimeout(() => element.handleClick());
    const { detail } = await oneEvent(element, 'readMore') as CustomEvent<Post>;
    expect(detail).to.be.equal(post);
  });
  • setTimeout() function schedules a click action over the web component.
  • Next, the oneEvent function helps to handle and resolve the expected event named "readMore". Once it's resolved, we should expect a CustomEvent<Post>.
  • When a Custom Event is fired, you can expect to have a detail attribute with an object. The Object destructuring would be useful in this case.
  • Finally, there's an assertion to verify the detail value. It should be the post set as an attribute for the Web Component.

Running the Tests

Just run the following commands:

  • npm run test, a single running of the whole suite of tests.
  • npm run test:watch, enable a watch mode. Especially useful to see the test results while you're performing changes into the test files.

The output for the first command would be as follows:

screenshot-output-testing-karma

Source Code Project

Find the complete project and these tests in this GitHub repository. Do not forget to give it a star ⭐️ and play around with the code.

You can follow me on Twitter and GitHub to see more of my work.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Drizzle ORM: A performant and type-safe alternative to Prisma cover image

Drizzle ORM: A performant and type-safe alternative to Prisma

Introduction I’ve written an article about a similar, more well-known TypeScript ORM named Prisma in the past. While it is a fantastic library that I’ve used and have had success with personally, I noted a couple things in particular that I didn’t love about it. Specifically, how it handles relations with add-on queries and also its bulk that can slow down requests in Lambda and other similar serverless environments. Because of these reasons, I took notice of a newer player in the TypeScript ORM space named Drizzle pretty quickly. The first thing that I noticed about Drizzle and really liked is that even though they call it an ‘ORM’ it’s more of a type-safe query builder. It reminds me of a JS query builder library called ‘Knex’ that I used to use years ago. It also feels like the non-futuristic version of EdgeDB which is another technology that I’m pretty excited about, but committing to it still feels like a gamble at this stage in its development. In contrast to Prisma, Drizzle is a ‘thin TypeScript layer on top of SQL’. This by default should make it a better candidate for Lambda’s and other Serverless environments. It could also be a hard sell to Prisma regulars that are living their best life using the incredibly developer-friendly TypeScript API’s that it generates from their schema.prisma files. Fret not, despite its query-builder roots, Drizzle has some tricks up its sleeve. Let’s compare a common query example where we fetch a list of posts and all of it’s comments from the Drizzle docs: ` // Drizzle query const posts = await db.query.posts.findMany({ with: { comments: true, }, }); // Prisma query const posts = await prisma.post.findMany({ include: { comments: true, }, }); ` Sweet, it’s literally the same thing. Maybe not that hard of a sale after all. You will certainly find some differences in their APIs, but they are both well-designed and developer friendly in my opinion. The schema Similar to Prisma, you define a schema for your database in Drizzle. That’s pretty much where the similarities end. In Drizzle, you define your schema in TypeScript files. Instead of generating an API based off of this schema, Drizzle just infers the types for you, and uses them with their TypeScript API to give you all of the nice type completions and things we’re used to in TypeScript land. Here’s an example from the docs: ` import { integer, pgEnum, pgTable, serial, uniqueIndex, varchar } from 'drizzle-orm/pg-core'; // declaring enum in database export const popularityEnum = pgEnum('popularity', ['unknown', 'known', 'popular']); export const countries = pgTable('countries', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), }, (countries) => { return { nameIndex: uniqueIndex('nameidx').on(countries.name), } }); export const cities = pgTable('cities', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), countryId: integer('countryid').references(() => countries.id), popularity: popularityEnum('popularity'), }); ` I’ll admit, this feels a bit clunky compared to a Prisma schema definition. The trade-off for a lightweight TypeScript API to work with your database can be worth the up-front investment though. Migrations Migrations are an important piece of the puzzle when it comes to managing our applications databases. Database schemas change throughout the lifetime of an application, and the steps to accomplish these changes is a non-trivial problem. Prisma and other popular ORMs offer a CLI tool to manage and automate your migrations, and Drizzle is no different. After creating new migrations, all that is left to do is run them. Drizzle gives you the flexibility to run your migrations in any way you choose. The simplest of the bunch and the one that is recommended for development and prototyping is the drizzle-kit push command that is similar to the prisma db push command if you are familiar with it. You also have the option of running the .sql files directly or using the Drizzle API's migrate function to run them in your application code. Drizzle Kit is a companion CLI tool for managing migrations. Creating your migrations with drizzle-kit is as simple as updating your Drizzle schema. After making some changes to your schema, you run the drizzle-kit generate command and it will generate a migration in the form of a .sql file filled with the needed SQL commands to migrate your database from point a → point b. Performance When it comes to your database, performance is always an extremely important consideration. In my opinion this is the category that really sets Drizzle apart from similar competitors. SQL Focused Tools like Prisma have made sacrifices and trade-offs in their APIs in an attempt to be as database agnostic as possible. Drizzle gives itself an advantage by staying focused on similar SQL dialects. Serverless Environments Serverless environments are where you can expect the most impactful performance gains using Drizzle compared to Prisma. Prisma happens to have a lot of content that you can find on this topic specifically, but the problem stems from cold starts in certain serverless environments like AWS Lambda. With Drizzle being such a lightweight solution, the time required to load and execute a serverless function or Lambda will be much quicker than Prisma. Benchmarks You can find quite a few different open-sourced benchmarks of common database drivers and ORMs in JavaScript land. Drizzle maintains their own benchmarks on GitHub. You should always do your own due diligence when it comes to benchmarks and also consider the inputs and context. In Drizzle's own benchmarks, it’s orders of magnitudes faster when compared to Prisma or TypeORM, and it’s not far off from the performance you would achieve using the database drivers directly. This would make sense considering the API adds almost no overhead, and if you really want to achieve driver level performance, you can utilize the prepared statements API. Prepared Statements The prepared statements API in Drizzle allows you to pre-generate raw queries that get sent directly to the underlying database driver. This can have a very significant impact on performance, especially when it comes to larger, more complex queries. Prepared statements can also provide huge performance gains when used in serverless environments because they can be cached and reused. JOINs I mentioned at the beginning of this article that one of the things that bothered me about Prisma is the fact that fetching relations on queries generates additional sub queries instead of utilizing JOINs. SQL databases are relational, so using JOINs to include data from another table in your query is a core and fundamental part of how the technology is supposed to work. The Drizzle API has methods for every type of JOIN statement. Properly using JOINs instead of running a bunch of additional queries is an important way to get better performance out of your queries. This is a huge selling point of Drizzle for me personally. Other bells and whistles Drizzle Studio UIs for managing the contents of your database are all the rage these days. You’ve got Prisma Studio and EdgeDB UI to name a couple. It's no surprise that these are so popular. They provide a lot of value by letting you work with your database visually. Drizzle also offers Drizzle Studio and it’s pretty similar to Prisma Studio. Other notable features - Raw Queries - The ‘magic’ sql operator is available to write raw queries using template strings. - Transactions - Transactions are a very common and important feature in just about any database tools. It’s commonly used for seeding or if you need to write some other sort of manual migration script. - Schemas - Schemas are a feature specifically for Postgres and MySQL database dialects - Views -Views allow you to encapsulate the details of the structure of your tables, which might change as your application evolves, behind consistent interfaces. - Logging - There are some logging utilities included useful for debugging, benchmarking, and viewing generated queries. - Introspection - There are APIs for introspecting your database and tables - Zod schema generation - This feature is available in a companion package called drizzle-zod that will generate Zod schema’s based on your Drizzle tables Seeding At the time of this writing, I’m not aware of Drizzle offering any tools or specific advice on seeding your database. I assume this is because of how straightforward it is to handle this on your own. If I was building a new application I would probably provide a simple seed script in JS or TS and use a runtime like node to execute it. After that, you can easily add a command to your package.json and work it into your CI/CD setup or anything else. Conclusion Drizzle ORM is a performant and type-safe alternative to Prisma. While Prisma is a fantastic library, Drizzle offers some advantages such as a lightweight TypeScript API, a focus on SQL dialects, and the ability to use JOINs instead of generating additional sub queries. Drizzle also offers Drizzle Studio for managing the contents of your database visually, as well as other notable features such as raw queries, transactions, schemas, views, logging, introspection, and Zod schema generation. While Drizzle may require a bit more up-front investment in defining your schema, it can be worth it for the performance gains, especially in serverless environments....

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators cover image

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators

Introduction: Welcome back to our blog series on Functional Programming in TypeScript using the fp-ts library. In the previous three blog posts, we covered essential concepts such as the pipe and flow operators, Option type, and various methods and operators like fold, fromNullable, getOrElse, map, flatten, and chain. In this fourth post, we will delve into the powerful Task and TaskEither operators, understanding their significance, and exploring practical examples to showcase their usefulness. Understanding Task and TaskEither: Before we dive into the examples, let's briefly recap what Task and TaskEither are and why they are valuable in functional programming. Task: In functional programming, a Task represents an asynchronous computation that may produce a value or an error. It allows us to work with asynchronous operations in a pure and composable manner. Tasks are lazy and only start executing when we explicitly run them. They can be thought of as a functional alternative to Promises. Now, let's briefly introduce the Either type and its significance in functional programming since this concept, merged with Task gives us the full power of TaskEither. Either: Either is a type that represents a value that can be one of two possibilities: a value of type Left or a value of type Right. Conventionally, the Left type represents an error or failure case, while the Right type represents a successful result. Using Either, we can explicitly handle and propagate errors in a functional and composable way. Example: Handling Division with Either Suppose we have a function divide that performs a division operation. Instead of throwing an error, we can use Either to handle the potential division by zero scenario. Here's an example: `ts import { Either, left, right } from 'fp-ts/lib/Either'; const divide: (a: number, b: number) => Either = (a, b) => { if (b === 0) { return left('Error: Division by zero'); } return right(a / b); }; const result = divide(10, 2); result.fold( (error) => console.log(Error: ${error}`), (value) => console.log(Result: ${value}`) ); ` In this example, the divide function returns an Either type. If the division is successful, it returns a Right value with the result. If the division by zero occurs, it returns a Left value with an error message. We then use the fold function to handle both cases, printing the appropriate message to the console. TaskEither: TaskEither combines the benefits of both Task and Either. It represents an asynchronous computation that may produce a value or an error, just like Task, but also allows us to handle potential errors using the Either type. This enables us to handle errors in a more explicit and controlled manner. Examples: Let's explore some examples to better understand the practical applications of Task and TaskEither operators. Example 1: Fetching Data from an API Suppose we want to fetch data from an API asynchronously. We can use the Task operator to encapsulate the API call and handle the result using the Task's combinators. In the example below, we define a fetchData` function that returns a Task representing the API call. We then use the `fold` function to handle the success and failure cases of the Task. If the Task succeeds, we return a new Task with the fetched data. If it fails, we return a Task with an error message. Finally, we use the `getOrElse` function to handle the case where the Task returns `None`. `typescript import { pipe } from 'fp-ts/lib/function'; import { Task } from 'fp-ts/lib/Task'; import { fold } from 'fp-ts/lib/TaskEither'; import { getOrElse } from 'fp-ts/lib/Option'; const fetchData: Task = () => fetch('https://api.example.com/data'); const handleData = pipe( fetchData, fold( () => Task.of('Error: Failed to fetch data'), (data) => Task.of(Fetched data: ${data}`) ), getOrElse(() => Task.of('Error: Data not found')) ); handleData().then(console.log); ` Example 2: Performing Computation with Error Handling Let's say we have a function divide` that performs a computation and may throw an error. We can use TaskEither to handle the potential error and perform the computation asynchronously. In the example below, we define a `divideAsync` function that takes two numbers and returns a TaskEither representing the division operation. We use the `tryCatch` function to catch any potential errors thrown by the `divide` function. We then use the `fold` function to handle the success and failure cases of the TaskEither. If the TaskEither succeeds, we return a new TaskEither with the result of the computation. If it fails, we return a TaskEither with an error message. Finally, we use the `map` function to transform the result of the TaskEither. `typescript import { pipe } from 'fp-ts/lib/function'; import { TaskEither, tryCatch } from 'fp-ts/lib/TaskEither'; import { fold } from 'fp-ts/lib/TaskEither'; import { map } from 'fp-ts/lib/TaskEither'; const divide: (a: number, b: number) => number = (a, b) => { if (b === 0) { throw new Error('Division by zero'); } return a / b; }; const divideAsync: (a: number, b: number) => TaskEither = (a, b) => tryCatch(() => divide(a, b), (error) => new Error(String(error))); const handleComputation = pipe( divideAsync(10, 2), fold( (error) => TaskEither.left(Error: ${error.message}`), (result) => TaskEither.right(Result: ${result}`) ), map((result) => Computation: ${result}`) ); handleComputation().then(console.log); ` In the first example, we saw how to fetch data from an API using Task and handle the success and failure cases using fold and getOrElse functions. This allows us to handle different scenarios, such as successful data retrieval or error handling when the data is not available. In the second example, we demonstrated how to perform a computation that may throw an error using TaskEither. We used tryCatch to catch potential errors and fold to handle the success and failure cases. This approach provides a more controlled way of handling errors and performing computations asynchronously. Conclusion: In this blog post, we explored the Task` and `TaskEither` operators in the `fp-ts` library. We learned that Task allows us to work with asynchronous computations in a pure and composable manner, while TaskEither combines the benefits of Task and Either, enabling us to handle potential errors explicitly. By leveraging the concepts we have covered so far, such as pipe, flow, Option, fold, map, flatten, and chain, we can build robust and maintainable functional programs in TypeScript using the fp-ts library. Stay tuned for the next blog post in this series, where we will continue our journey into the world of functional programming....

How to Manage Breakpoints using BreakpointObserver in Angular cover image

How to Manage Breakpoints using BreakpointObserver in Angular

Defining Breakpoints is important when you start working with Responsive Design and most of the time they're created using CSS code. For example: `css .title { font-size: 12px; } @media (max-width: 600px) { .title { font-size: 14px; } } ` By default, the text size value will be 12px, and this value will be changed to 14px when the viewport gets changed to a smaller screen (a maximum width of 600px). That solution works. However, what about if you need to listen_ for certain breakpoints to perform changes in your application? This may be needed to configure third-party components, processing events, or any other. Luckily, Angular comes with a handy solution for these scenarios: the BreakpointObserver. Which is a utility for checking the matching state of @media queries. In this post, we will build a sample application to add the ability to configure certain breakpoints, and being able to listen_ to them. Project Setup Prerequisites You'll need to have installed the following tools in your local environment: - Node.js**. Preferably the latest LTS version. - A package manager**. You can use either NPM or Yarn. This tutorial will use NPM. Creating the Angular Project Let's start creating a project from scratch using the Angular CLI tool. `bash ng new breakpointobserver-example-angular --routing --prefix corp --style css --skip-tests ` This command will initialize a base project using some configuration options: - --routing`. It will create a routing module. - --prefix corp`. It defines a prefix to be applied to the selectors for created components(`corp` in this case). The default value is `app`. - --style scss`. The file extension for the styling files. - --skip-tests`. it avoids the generations of the `.spec.ts` files, which are used for testing Adding Angular Material and Angular CDK Before creating the breakpoints, let's add the Angular Material components, which will install the Angular CDK` library under the hood. `bash ng add @angular/material ` Creating the Home Component We can create a brand new component to handle a couple of views to be updated while the breakpoints are changing. We can do that using the ng generate` command. `bash ng generate component home ` Pay attention to the output of the previous command since it will show you the auto-generated files. Update the Routing Configuration Remember we used the flag --routing` while creating the project? That parameter has created the main routing configuration file for the application: `app-routing.module.ts`. Let's update it to be able to render the `home` component by default. `ts // app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; const routes: Routes = [ { path: '', component: HomeComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } ` Update the App Component template Remove all code except the router-outlet` placeholder: `html ` This will allow rendering the home` component by default once the routing configuration is running. Using the BreakpointObserver The application has the Angular CDK installed already, which has a layout` package with some utilities to build responsive UIs that _react_ to screen-size changes. Let's update the HomeComponent`, and inject the `BreakpointObserver` as follows. `ts //home.component.ts import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; @Component({ selector: 'corp-home', templateUrl: './home.component.html', styleUrls: ['./home.component.css'] }) export class HomeComponent implements OnInit { readonly breakpoint$ = this.breakpointObserver .observe([Breakpoints.Large, Breakpoints.Medium, Breakpoints.Small, '(min-width: 500px)']) .pipe( tap(value => console.log(value)), distinctUntilChanged() ); constructor(private breakpointObserver: BreakpointObserver) { } ngOnInit(): void { } } ` Once the BreakpointObserver` is injected, we'll be able to evaluate media queries to determine the current screen size, and perform changes accordingly. Then, a breakpoint$` variable references an _observable_ object after a call to the `observe` method. The observe** method gets an observable of results for the given queries, and can be used along with predetermined values defined on `Breakpoints` as a constant. Also, it's possible to use custom breakpoints such as (min-width: 500px)`. Please refer to the documentation to find more details about this. Next, you may need to subscribe_ to the `breakpoint$` observable to see the emitted values after matching the given queries. Again, let's update the home.component.ts` file to do that. `ts // home.component.ts // .... other imports import { distinctUntilChanged, tap } from 'rxjs/operators'; @Component({ //.... }) export class HomeComponent implements OnInit { Breakpoints = Breakpoints; currentBreakpoint:string = ''; // ... readonly breakpoint$ = this.breakpointObserver constructor(private breakpointObserver: BreakpointObserver) { } ngOnInit(): void { this.breakpoint$.subscribe(() => this.breakpointChanged() ); } private breakpointChanged() { if(this.breakpointObserver.isMatched(Breakpoints.Large)) { this.currentBreakpoint = Breakpoints.Large; } else if(this.breakpointObserver.isMatched(Breakpoints.Medium)) { this.currentBreakpoint = Breakpoints.Medium; } else if(this.breakpointObserver.isMatched(Breakpoints.Small)) { this.currentBreakpoint = Breakpoints.Small; } else if(this.breakpointObserver.isMatched('(min-width: 500px)')) { this.currentBreakpoint = '(min-width: 500px)'; } } } ` In the above code, the ngOnInit` method is used to perform a _subscription_ to the `breakpoint$` observable and the method `breakpointChanged` will be invoked every time a breakpoint match occurs. As you may note, the breakpointChanged` method verifies what Breakpoint value has a match through `isMatched` method. In that way, the current component can perform changes after a match happened (in this case, it just updates the value for the `currentBreakpoint` attribute). Using Breakpoint values on the Template Now, we can set a custom template in the home.component.html` file and be able to render a square according to the `currentBreakpoint` value. `html {{ currentBreakpoint }} Large Medium Small Custom ` The previous template will render the current media query value at the top along with a rectangle according to the size: Large, Medium, Small or Custom. Live Demo and Source Code Want to play around with this code? Just open the Stackblitz editor or the preview mode in fullscreen. Find the complete angular project in this GitHub repository: breakpointobserver-example-angular. Do not forget to give it a star ⭐️ and play around with the code. Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work....

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools cover image

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools

In the ever-evolving world of web development, Nuxt.js has taken a monumental leap with the launch of Nuxt DevTools v1.0. More than just a set of tools, it's a game-changer—a faithful companion for developers. This groundbreaking release, available for all Nuxt projects and being defaulted from Nuxt v3.8 onwards, marks the beginning of a new era in developer tools. It's designed to simplify our development journey, offering unparalleled transparency, performance, and ease of use. Join me as we explore how Nuxt DevTools v1.0 is set to revolutionize our workflow, making development faster and more efficient than ever. What makes Nuxt DevTools so unique? Alright, let's start delving into the features that make this tool so amazing and unique. There are a lot, so buckle up! In-App DevTools The first thing that caught my attention is that breaking away from traditional browser extensions, Nuxt DevTools v1.0 is seamlessly integrated within your Nuxt app. This ensures universal compatibility across browsers and devices, offering a more stable and consistent development experience. This setup also means the tools are readily available in the app, making your work more efficient. It's a smart move from the usual browser extensions, making it a notable highlight. To use it you just need to press Shift + Option + D` (macOS) or `Shift + Alt + D` (Windows): With simple keystrokes, the Nuxt DevTools v1.0 springs to life directly within your app, ready for action. This integration eliminates the need to toggle between windows or panels, keeping your workflow streamlined and focused. The tools are not only easily accessible but also intelligently designed to enhance your productivity. Pages, Components, and Componsables View The Pages, Components, and Composables View in Nuxt DevTools v1.0 are a clear roadmap for your app. They help you understand how your app is built by simply showing its structure. It's like having a map that makes sense of your app's layout, making the complex parts of your code easier to understand. This is really helpful for new developers learning about the app and experienced developers working on big projects. Pages View lists all your app's pages, making it easier to move around and see how your site is structured. What's impressive is the live update capability. As you explore the DevTools, you can see the changes happening in real-time, giving you instant feedback on your app's behavior. Components View is like a detailed map of all the parts (components) your app uses, showing you how they connect and depend on each other. This helps you keep everything organized, especially in big projects. You can inspect components, change layouts, see their references, and filter them. By showcasing all the auto-imported composables, Nuxt DevTools provides a clear overview of the composables in use, including their source files. This feature brings much-needed clarity to managing composables within large projects. You can also see short descriptions and documentation links in some of them. Together, these features give you a clear picture of your app's layout and workings, simplifying navigation and management. Modules and Static Assets Management This aspect of the DevTools revolutionizes module management. It displays all registered modules, documentation, and repository links, making it easy to discover and install new modules from the community! This makes managing and expanding your app's capabilities more straightforward than ever. On the other hand, handling static assets like images and videos becomes a breeze. The tool allows you to preview and integrate these assets effortlessly within the DevTools environment. These features significantly enhance the ease and efficiency of managing your app's dynamic and static elements. The Runtime Config and Payload Editor The Runtime Config and Payload Editor in Nuxt DevTools make working with your app's settings and data straightforward. The Runtime Config lets you play with different configuration settings in real time, like adjusting settings on the fly and seeing the effects immediately. This is great for fine-tuning your app without guesswork. The Payload Editor is all about managing the data your app handles, especially data passed from server to client. It's like having a direct view and control over the data your app uses and displays. This tool is handy for seeing how changes in data impact your app, making it easier to understand and debug data-related issues. Open Graph Preview The Open Graph Preview in Nuxt DevTools is a feature I find incredibly handy and a real time-saver. It lets you see how your app will appear when shared on social media platforms. This tool is crucial for SEO and social media presence, as it previews the Open Graph tags (like images and descriptions) used when your app is shared. No more deploying first to check if everything looks right – you can now tweak and get instant feedback within the DevTools. This feature not only streamlines the process of optimizing for social media but also ensures your app makes the best possible first impression online. Timeline The Timeline feature in Nuxt DevTools is another standout tool. It lets you track when and how each part of your app (like composables) is called. This is different from typical performance tools because it focuses on the high-level aspects of your app, like navigation events and composable calls, giving you a more practical view of your app's operation. It's particularly useful for understanding the sequence and impact of events and actions in your app, making it easier to spot issues and optimize performance. This timeline view brings a new level of clarity to monitoring your app's behavior in real-time. Production Build Analyzer The Production Build Analyzer feature in Nuxt DevTools v1.0 is like a health check for your app. It looks at your app's final build and shows you how to make it better and faster. Think of it as a doctor for your app, pointing out areas that need improvement and helping you optimize performance. API Playground The API Playground in Nuxt DevTools v1.0 is like a sandbox where you can play and experiment with your app's APIs. It's a space where you can easily test and try out different things without affecting your main app. This makes it a great tool for trying out new ideas or checking how changes might work. Some other cool features Another amazing aspect of Nuxt DevTools is the embedded full-featured VS Code. It's like having your favorite code editor inside the DevTools, with all its powerful features and extensions. It's incredibly convenient for making quick edits or tweaks to your code. Then there's the Component Inspector. Think of it as your code's detective tool. It lets you easily pinpoint and understand which parts of your code are behind specific elements on your page. This makes identifying and editing components a breeze. And remember customization! Nuxt DevTools lets you tweak its UI to suit your style. This means you can set up the tools just how you like them, making your development environment more comfortable and tailored to your preferences. Conclusion In summary, Nuxt DevTools v1.0 marks a revolutionary step in web development, offering a comprehensive suite of features that elevate the entire development process. Features like live updates, easy navigation, and a user-friendly interface enrich the development experience. Each tool within Nuxt DevTools v1.0 is thoughtfully designed to simplify and enhance how developers build and manage their applications. In essence, Nuxt DevTools v1.0 is more than just a toolkit; it's a transformative companion for developers seeking to build high-quality web applications more efficiently and effectively. It represents the future of web development tools, setting new standards in developer experience and productivity....