Skip to content

Testing Web Components with Cypress and TypeScript

Testing Web Components with Cypress and TypeScript

In my previous posts, I covered a wide range of topics about LitElement and TypeScript. Then I created a Single Page Application based on Web Components.

Let's dive deep into this project to understand how to add the End-to-End(E2E) testing capability using Cypress and TypeScript.

The Shadow DOM

The Shadow DOM API is about web components encapsulation, meaning it keeps the styling, markup, and behavior hidden and separate from other code on the page. This API provides a way to attach a separated DOM to an element.

shadow-dom

To clarify, let's consider the /about page from our application:

shadow-root-screenshot

As you can see, The Shadow DOM allows hidden DOM trees and starts with a shadow root: #shadow-root (open). The word open means you can access the shadow DOM through JavaScript.

However, it's feasible to attach a shadow root with a closed mode, meaning you won't be able to access the shadow DOM. It's a really good way to encapsulate an inner structure or implementation.

Adding Cypress Support

Installing Cypress

Install Cypress as part of the development dependencies:

npm install --save-dev cypress

This will produce the following output:

litelement-website$ npm install --save-dev cypress

Installing Cypress (version: 5.3.0)

  ✔  Downloaded Cypress
  ✔  Unzipped Cypress
  ✔  Finished Installation /Users/luixaviles/Library/Caches/Cypress/5.3.0

You can now open Cypress by running: node_modules/.bin/cypress open

https://on.cypress.io/installing-cypress

After runing the Cypress installer, you'll find the following folder structure:

|- lit-element-website/
    |- cypress/
        |- fixtures/
        |- integrations/
        |- plugins/
        |- support/

By default, you'll have several JavaScript files in those new folders, including examples and configurations. You may decide to keep those examples for Cypress reference or remove them from the project.

Adding The TypeScript Configuration

Since we have a TypeScript-based project, we can consider the new cypress directory as the root of a new sub-project for End-to-End testing.

Let's create the /cypress/tsconfig.json file as follows:

{
  "compilerOptions": {
    "strict": true,
    "baseUrl": "../node_modules",
    "target": "es6",
    "lib": ["es5", "es6", "dom", "dom.iterable"],
    "types": ["cypress"]
  },
  "include": ["**/*.ts"]
}

This file will enable the cypress directory as a TypeScript project and we'll be ready to set more properties and configurations if that is needed in the future.

Cypress Configurations

Next, we'll update the autogenerated cypress.json file as follows:

{
  "supportFile": "cypress/support/index.ts",
  "experimentalShadowDomSupport": true
}
  • The supportFile parameter will set up a path to the file to be loaded before loading test files.
  • The experimentalShadowDomSupport flag was needed to enable shadow DOM testing in previous versions. However, since v5.2.0 it is no longer necessary.

Webpack Configuration and TypeScript Compilation

First, install the following tools:

npm install --save-dev webpack typescript ts-loader

Since we'll use TypeScript for our Cypress tests, we'll be using Webpack and ts-loader to compile and process these files.

The Cypress Webpack preprocessor

Install the cypress-webpack-preprocessor:

npm install --save-dev @cypress/webpack-preprocessor

Update the content of plugins/index.js file to:

const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');

module.exports = on => {
  on('file:preprocessor', cypressTypeScriptPreprocessor);
};

On other hand, create a JavaScript file: cypress/plugins/cy-ts-preprocessor.js with the Webpack configuration:

const wp = require('@cypress/webpack-preprocessor');

const webpackOptions = {
  resolve: {
    extensions: ['.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: 'ts-loader',
          },
        ],
      },
    ],
  },
};

const options = {
  webpackOptions,
};

module.exports = wp(options);

At this point, make sure the file structure is as shown below:

|- lit-element-website/
    |- cypress/
        |- fixtures/
        |- integrations/
        |- plugins/
            |- index.js
            |- cy-ts-preprocessor.js
        |- support/
            |- commands.ts
            |- index.ts
        |- tsconfig.json
    |- cypress.json

Adding npm scripts

Add the following scripts into package.json file:

{
  "scripts": {
    "cypress:run": "cypress run --headless --browser chrome",
    "cypress:open": "cypress open"
  }
}
  • cypress:run defines a script to run all End-to-End tests in a headless mode in the command line. That means the browser will be hidden.
  • cypress:open Will fore Electron to be shown. You can use cypress run --headed as another option with the same effect.

You can see all available parameters to run commands on Cypress here.

Adding the Tests

You should be ready to add your test files at this point.

Testing the About Page

Let's create our first test file into cypress/integration folder as about.spec.ts:

// about.spec.ts
describe('About Page', () => {
  beforeEach(() => {
    const baseUrl = 'http://localhost:8000/about';
    cy.visit(baseUrl);
  });

  it("should show 'LitElement Website' as title", () => {
    cy.title().should('eq', 'LitElement Website');
  });

  it("should read 'About Me' inside custom element", () => {
    cy.get('lit-about')
      .shadow()
      .find('h2')
      .should('contain.text', 'About Me');
  });
});

The previous file defines two test cases. The second one expects to have access to the Shadow DOM through .shadow() function, which yields the new DOM element(s) it found. Read more about this syntax here.

shadow-root-screenshot

As this screenshot shows, the h2 element contains the title of the About page and we can use contain.text to complete the assertion.

In case you want to take more control of the DOM content of your web component, you can try something like this instead:

it("should read 'About Me' inside custom element ", () => {
    cy.get('lit-about')
      .shadow()
      .find('h2')
      .should(e => {
        const [h2] = e.get();
        // Here we have the control of DOM conten from custom element
        console.log('h2!', h2, h2.textContent);
        expect(h2.textContent).to.contains('About Me');
      });
  });

Testing the Blog Posts Page

Let's create a new TypeScript file inside cypress/integration folder as blog-posts.spec.ts:

// blog-posts.spec.ts

describe('Blog Posts Page', () => {
  beforeEach(() => {
    const baseUrl = 'http://localhost:8000/blog';
    cy.visit(baseUrl);
  });

  it("should read 'Blog Posts' as title", () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('h2')
      .should('contain.text', 'Blog Posts');
  });

  it("should display a list of blog cards", () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('blog-card')
      .its('length')
      .should('be.gt', 0);
  });
});

In this case, we expect to have some blog-card elements inside the Blog Posts page.

Testing the Blog Card Elements

Let's create a new TypeScript file inside cypress/integration folder as blog-card.spec.ts:

// blog-card.spec.ts

describe('Blog Card', () => {
  const titles = [
    'Web Components Introduction',
    'LitElement with TypeScript',
    'Navigation and Routing with Web Components',
  ];

  const author = 'Luis Aviles';

  beforeEach(() => {
    const baseUrl = 'http://localhost:8000/blog';
    cy.visit(baseUrl);
  });

  it('should display a title', () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('blog-card')
      .each((item, i) => {
        cy.wrap(item)
        .shadow()
        .find('h1')
        .should('contain.text', titles[i]);
      });
  });

  it('should display the author\'s name', () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('blog-card')
      .each((item, i) => {
        cy.wrap(item)
        .shadow()
        .find('h2')
        .should('contain.text', author);
      });
  });
});

The Blog Card scenario defines the title values for every blog post and the author's name for all of them.

  • The should display a title test case iterates through all blog-card elements and access(again) to the Shadow DOM through .shadow() function to compare the title value.
  • The should display the author's name applies the same logic as above to verify the author's name.

See more details in the following screenshot:

spec-blog-card

Running the Tests

Some tests have been implemented already. Let's run the scripts we defined before as follows:

npm run cypress:run

As stated before, the previous script will run all End-to-End(E2E) tests entirely on the command line.

npm run cypress:open

This command will open the Cypress Test Runner through the Electron window.

Source Code Project

Find the complete project in this GitHub repository: https://github.com/luixaviles/litelement-website. 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 about 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

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights) cover image

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights)

We take accessibility quite seriously here at This Dot because we know it's important. Still, throughout my career, I've seen many projects where accessibility was brushed aside for reasons like "our users don't really use keyboard shortcuts" or "we need to ship fast; we can add accessibility later." The truth is, that "later" often means "never." And it turns out, anyone could break their hand, like I did. I broke my dominant hand and spent four weeks in a cast, effectively rendering it useless and forcing me to work left-handed. I must thus apologize for the misleading title; this post should more accurately be dubbed "second-hand" accessibility insights. The Perspective of a Developer Firstly, it's not the end of the world. I adapted quickly to my temporary disability, which was, for the most part, a minor inconvenience. I had to type with one hand, obviously slower than my usual pace, but isn't a significant part of a software engineer's work focused on thinking? Here's what I did and learned: - I moved my mouse to the left and started using it with my left hand. I adapted quickly, but the experience wasn't as smooth as using my right hand. I could perform most tasks, but I needed to be more careful and precise. - Many actions require holding a key while pressing a mouse button (e.g., visiting links from the IDE), which is hard to do with one hand. - This led me to explore trackpad options. Apart from the Apple Magic Trackpad, choices were limited. As a Windows user (I know, sorry), that wasn't an option for me. I settled for a cheap trackpad from Amazon. A lot of tasks became easier; however, the trackpad eventually malfunctioned, sending me back to the mouse. - I don't know a lot of IDE shortcuts. I realized how much I've been relying on a mouse for my work, subconsciously refusing to learn new keyboard shortcuts (I'll be returning my senior engineer license shortly). So I learned a few new ones, which is good, I guess. - Some keyboard shortcuts are hard to press with one hand. If you find yourself in a similar situation, you may need to remap some of them. - Copilot became my best friend, saving me from a lot of slow typing, although I did have to correct and rewrite many of its suggestions. The Perspective of a User As a developer, I was able to get by and figure things out to be able to work effectively. As a user, however, I got to experience the other side of the coin and really feel the accessibility (or lack thereof) on the web. Here are a few insights I gained: - A lot of websites apparently tried_ to implement keyboard navigation, but failed miserably. For example, a big e-commerce website I tried to use to shop for the aforementioned trackpad seemed to work fine with keyboard navigation at first, but once I focused on the search field, I found myself unable to tab out from it. When you make the effort to implement keyboard navigation, please make sure it works properly and it doesn't get broken with new changes. I wholeheartedly recommend having e2e tests (e.g. with Playwright) that verify the keyboard navigation works as expected. - A few websites and web apps I tried to use were completely unusable with the keyboard and were designed to be used with a mouse only. - Some sites had elaborate keyboard navigation, with custom keyboard shortcuts for different functionality. That took some time to figure out, and I reckon it's not as intuitive as the designers thought it would be. Once a user learns the shortcuts, however, it could make their life easier, I suppose. - A lot of interactive elements are much smaller than they should be, making it hard to accurately click on them with your weaker hand. Designers, I beg you, please make your buttons bigger. I once worked on an application that had a "gloves mode" for environments where the operators would be using gloves, and I feel like maybe the size we went with for the "gloves mode" should be the standard everywhere, especially as screens get bigger and bigger. - Misclicking is easy, especially using your weaker hand. Be it a mouse click or just hitting an Enter key on accident. Kudos to all the developers who thought about this and implemented a confirmation dialog or other safety measures to prevent users from accidentally deleting or posting something. I've however encountered a few apps that didn't have any of these, and those made me a bit anxious, to be honest. If this is something you haven't thought about when developing an app, please start doing so, you might save someone a lot of trouble. Some Second-Hand Insights I was only a little bit impaired by being temporarily one-handed and it was honestly a big pain. In this post, I've focused on my anecdotal experience as a developer and a user, covering mostly keyboard navigation and mouse usage. I can only imagine how frustrating it must be for visually impaired users, or users with other disabilities, to use the web. I must confess I haven't always been treating accessibility as a priority, but I've certainly learned my lesson. I will try to make sure all the apps I work on are accessible and inclusive, and I will try to test not only the keyboard navigation, ARIA attributes, and other accessibility features, but also the overall experience of using the app with a screen reader. I hope this post will at least plant a little seed in your head that makes you think about what it feels like to be disabled and what would the experience of a disabled person be like using the app you're working on. Conclusion: The Humbling Realities of Accessibility The past few weeks have been an eye-opening journey for me into the world of accessibility, exposing its importance not just in theory but in palpable, daily experiences. My short-term impairment allowed me to peek into a life where simple tasks aren't so simple, and convenient shortcuts are a maze of complications. It has been a humbling experience, but also an illuminating one. As developers and designers, we often get caught in the rush to innovate and to ship, leaving behind essential elements that make technology inclusive and humane. While my temporary disability was an inconvenience, it's permanent for many others. A broken hand made me realize how broken our approach towards accessibility often is. The key takeaway here isn't just a list of accessibility tips; it's an earnest appeal to empathize with your end-users. "Designing for all" is not a checkbox to tick off before a product launch; it's an ongoing commitment to the understanding that everyone interacts with technology differently. When being empathetic and sincerely thinking about accessibility, you never know whose life you could be making easier. After all, disability isn't a special condition; it's a part of the human condition. And if you still think "Our users don't really use keyboard shortcuts" or "We can add accessibility later," remember that you're not just failing a compliance checklist, you're failing real people....

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...

How to Update the Application Title based on Routing Changes in Angular cover image

How to Update the Application Title based on Routing Changes in Angular

Have you tried to update the document's title of your application? Maybe you're thinking that applying interpolation should be enough: `html {{myCustomTitleVariable}} ` That solution is not going to work since the ` element is outside of the scope of the Angular application. In fact, the root component of your app is within `` tag, and the title is part of the `` element. Luckily, Angular provides the Title service with the methods to read the current title of the application, and a setTitle(title)` to update that value. However, what happens if you need to update the title on routing changes? Also, you may consider updating it on certain components for Analytics purposes. In this blog post, I'll explain step-by-step how to create a custom Title service to have full control over the title of the current HTML document for your application. 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 assume we'll need to build an application with the following routes as requirements: `txt /home |- Renders a home component /products |- Renders a list of products /products/ |- Renders a product detail based on its Identifier The app redirects to /home path by default ` Now, let's create the project from scratch using the Angular CLI tool. `bash ng new angular-update-title --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 css`. The file extension for the styling files. - --skip-tests`. it avoids the generations of the `.spec.ts` files, which are used for testing Creating the Modules and Components Once we got the initial structure of the app, we'll continue running the following commands to create a separate module for /home` and `/products`, which are the main paths of the project: `bash ng generate module home --routing ng generate component home/home ng generate module products --routing ng generate component products/products ng generate component products/product-detail ` The `--routing` flag can be using also along with `ng generate module` to create a routing configuration file for that module. Creating the Title Service Similar to the previous section, we will create a shared` module to hold the `Title` service. Both can be generated with the following commands: `bash ng generate module shared --module app ng generate service shared/services/title ` The `--module app` flag is used to "link" the brand new module to the pre-existing `app.module.ts` file. The Routing Configuration Open the app-routing.module.ts` file, and create the initial routes. `ts // app-routing.module.ts const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'home' }, { path: 'home', component: HomeComponent, data: { pageTitle: 'Home' } }, { path: 'products', loadChildren: () => import('./products/products.module').then(m => m.ProductsModule) } ]; ` By default, the application will redirect to the `home` path. When the router loads the `home` path, a `HomeComponent` will be rendered. The `products` path will be loaded using the _lazy loading_ feature. Pay attention to the data provided to the home` path. It contains the configured title through `pageTitle` string. Next, open the products-routing.module.ts` file to enable an additional configuration to load the _Products_ and the _Product Detail_ page. `ts // products-routing.module.ts const routes: Routes = [ { path: '', component: ProductsComponent, children: [ { path: ':id', component: ProductDetailComponent, } ], data: { pageTitle: 'Products' } }, ]; ` The router will render the `ProductsComponent` by default when the path matches to `/products`. This route also defines custom data to be rendered as titles later. When the path also adds an Id on `/products/:id`, the router will render the `ProductDetailComponent`. The Title Service Implementation It's time to implement the custom Title Service for our application. `ts // title.service.ts import { Injectable } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { BehaviorSubject, merge, Observable } from 'rxjs'; import { filter, map, tap } from 'rxjs/operators'; const DEFAULTTITLE = 'Corp'; @Injectable({ providedIn: 'root' }) export class TitleService { title$ = new BehaviorSubject(DEFAULTTITLE); private titleRoute$: Observable = this.router.events.pipe( filter((event) => event instanceof NavigationEnd), map(() => this.getPageTitle(this.activatedRoute.firstChild)) ); private titleState$ = merge(this.title$, this.titleRoute$).pipe( filter((title) => title !== undefined), tap((title) => { this.titleService.setTitle(${DEFAULT_TITLE} - ${title}`); }) ); constructor( private router: Router, private activatedRoute: ActivatedRoute, private titleService: Title ) { this.titleState$.subscribe(); } private getPageTitle( activatedRoute: ActivatedRoute | null ): string | undefined { while (activatedRoute) { if (activatedRoute.firstChild) { activatedRoute = activatedRoute.firstChild; } else if ( activatedRoute.snapshot.data && activatedRoute.snapshot.data['pageTitle'] ) { return activatedRoute.snapshot.data['pageTitle'] as string; } else { return undefined; } } return undefined; } } ` The above service implementation could be understood in just a few steps. First, we'll need to make sure to inject the `Router`, `ActivatedRoute` and `Title` services in the constructor. The `title$` attribute contains the initial value for the title("Corp"), which will be emitted through a _BehaviorSubject_. The `titleRoute$` is an Observable ready to emit any `pageTitle` value defined in the current route. It may use the parent's _pageTitle_ otherwise. The `titleState$` is an Observable ready to _listen_ to either `title$` or `titleRoute$` values. In case incoming value is defined, it will call the Angular Title service to perform the update. The `getPageTitle` method will be in charge of obtaining the `pageTitle` of the current route if it is defined or the title of the parent otherwise. Injecting the Title Service One easy way to apply the custom Title Service in the whole application is by updating the app.module.ts` file and injecting it into the constructor. `ts // app.module.ts export class AppModule { constructor(public titleService: TitleService) {} } ` In that way, once the default component gets rendered, the title will be displayed as Corp - Home`. If you click on Go to Products_ link, then a redirection will be performed and the Title service will be invoked again to display `Corp - Products` at this time. However, we may need to render a different title according to the product detail. In this case, we'll show Corp - Product Detail - :id` where the `Id` matches with the current route parameter. `ts // product-detail.component.ts import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { map, Subscription, tap } from 'rxjs'; import { TitleService } from 'src/app/shared/services/title.service'; @Component({ selector: 'corp-product-detail', templateUrl: './product-detail.component.html', styleUrls: ['./product-detail.component.css'], }) export class ProductDetailComponent implements OnInit, OnDestroy { protected subscription = new Subscription(); productId$ = this.route.params.pipe(map((params) => params['id'])); constructor( private route: ActivatedRoute, private titleService: TitleService ) {} ngOnInit(): void { const productIdSubscription = this.productId$ .pipe( tap((id) => this.titleService.title$.next(Product Detail - ${id}`)) ) .subscribe(); this.subscription.add(productIdSubscription); } ngOnDestroy(): void { this.subscription.unsubscribe(); } } ` Let's explain the implementation of this component: The constructor injects the `ActivatedRoute` and the custom `TitleService`. The `productId$` is the _Observable_ which is going to emit the `Id` parameter every time it changes in the URL. Once the component gets initialized, we'll need to _subscribe_ to the `productId$` _Observable_ and then emit a new value for the title after creating a new string using the `id`. That's possible through the `titleService.title$.next()` method. When the component gets _destroyed_, we'll need to _unsubscribe_ from the `productIdSubscription`. We're ready to go! Every time you select a product, the ProductDetail` component will be rendered, and the title will be updated accordingly. Live Demo and Source Code Want to play around with the final application? Just open the following link in your browser: https://luixaviles.github.io/angular-update-title. Find the complete angular project in this GitHub repository: angular-update-title-service. 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....

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....