Tag: directives

  • Why Angular components, pipes, and directives should stand alone and what that means for modules

    Why Angular components, pipes, and directives should stand alone and what that means for modules

    Don’t get me wrong – I really love Angular modules. They are like that office manager who keeps track of everything in their head and runs everything like a nearly infallible machine.

    But if you are developing a UI component library, it gets very tedious to have to create a module just so something can use your one component. Modules are meant for groupings, and they are great at that. But for generic components, they can be overkill.

    What difference does a module make?

    Before standalone, the only way to use a component, directive, or pipe was to declare them in a module that had to be imported by whoever wanted to use them. If you declared multiple items in the same module, it became hard to track what was being imported for what and what was no longer being used. This ran the risk of importing things unnecessarily, causing bloated bundle sizes and subsequently slower app times.

    As of Angular 14, the brilliant Angular team added standalone functionality. In other words, components, directives, and pipes can import what they need directly without modules just like any other import.

    You can import components, directives or pipes directly to use them!

    Lazy load with less files

    Before standalone, the only way to lazy load was to create a module with routing to serve as a locator to get to the correct component without any knowledge of what that component is. Now we can point to the component with imports instead of routes.

    Should I switch everything to standalone?

    I can’t think of a good reason to declare components, directives, and pipes with modules ever again. Please let me know if you can think of reasons why you should.

    What good are modules then?

    Modules are still super useful, just less needed…like a lot less needed. Actually, if you use them just for grouping and are no longer concerned with the dependencies of what you’re declaring, how to best use modules will become a lot clearer.

    For example, if you’re importing the same group of components, directives, and/or pipes across multiple places or want to alter what’s being imported in one spot, you should bundle them in a module. Don’t use the module to declare anything. Only use it for importing/exporting what you need.

    Another example would be if you are using things that rely on each other, like a component that uses a directive to interact with its children. But even then, just use the module to import/export.

    Which Angular decorators can be standalone?

    • Components
    • Directives
    • Pipes

    How do you become standalone?

    By Angular CLI

    ng g c componentName --standalone
    

    By component metadata

    @Component({
      standalone: true,
      selector: 'your-selector',
      imports: [
        CommonModule,
        ExampleModule
      ],
      ...
    })
    

    If you’re using an existing decorator, you need to delete it from the module that’s declaring it. You can be declared by a module or have standalone turned on but not both.

  • Allow users to edit the content of any element with this Angular directive

    Allow users to edit the content of any element with this Angular directive

    As a side project, I am working on a note taker/learning aid app built with Angular. One of my main goals is to make it as user friendly as possible, so I decided to make an Angular directive that does the following:

    1. Lets user click on text to edit the contents
    2. Can restrict edits by requiring user to hit the edit icon first, essentially entering an edit mode
    3. Checks whether the text was altered when the input loses focus or when the user hits ‘Enter’
    4. Saves the text if it was altered

    To begin, the directive has the following properties:

      // Use this variable to store the original value
      value: string;
      
      // Use this input if you want to restrict editing to only be allowed in edit mode 
      @Input() allowEdit: boolean = true;
      
      // Use this input if you are saving the change and need to reference the identifier of the change
      @Input() id: number;
      
      // Use this input to indicate what field is being changed
      @Input() update: string;
      
      // Use this output to notify the parent that a change has been made
      @Output() onChange: EventEmitter<EditUpdate> = new EventEmitter();
      
    

    For the constructor, you will need:

      // Automatically get a reference to the element you're changing with ElementRef
      private el: ElementRef
      
      // Safely update the element without accessing the DOM directly by using Renderer2
      private renderer: Renderer2
      
    

    Detect events with HostListener

    The beauty of using HostListener is that you don’t have to keep track of who is sending the event because as the name implies, it’s the host of whatever you added the directive to. (To me, this is the true power of directives!)

    That means no more redundant code of individually assigning event listeners and then hooking those up to the functions that contain callbacks of what you want to happen when those events are made! No more having to build hierarchical relationships so that you can modify the element in relation to the elements around it!

    I have three HostListeners for this directive because I want to detect when the user clicks on the element, when the user clicks off of the element, and when the user hits ‘Enter’ on the keyboard.

    A HostListener to detect click events

    If you want to toggle editing capabilities, wrap an if statement around the function that’s handling the click event. Because the parent determines whether or not the element should be editable, you don’t have to worry about toggling this variable. (I’ll cover how to reset the variable further down this article.)

    @HostListener("click") onClick() {
      if (this.allowEdit) {
        this.makeEditable();
      }
    }
    

    Some reasons to make editing not automatic by default

    1. You want only certain users with the proper permissions to be allowed to edit the text.
    2. You want the text to be selectable when it’s not being editing.
    3. You want to limit the possibility of accidental edits.

    HostListeners to detect when the user is done editing

    When first making this directive, I spent a lot of time creating a save/undo button interaction that hid/showed other functionality in the different modes but then later scrapped that approach because it required the user to click unnecessarily. After all, nothing is more frustrating than losing all of your work because you forgot to hit save.

    I created one for when the user clicks elsewhere and also another to listen for the ‘Enter’ key.

     @HostListener('blur') blurred() {
      this.checkValues();
    }
    
    @HostListener('keydown', [('$event')]) onKeydown(event) {
      if (event.keyCode === 13) {
         event.preventDefault();
         this.checkValues();
    
      }
    }
    

    Enter Edit Mode

    You can use ElementRef to identify the element rather than querying the DOM and Renderer2 to manipulate it. This function simply adds the editing class to the element, thereby providing visual feedback to the user that it’s now editable.

    makeEditable() {
      this.renderer.addClass(this.el.nativeElement, "editing");
      this.renderer.setAttribute(this.el.nativeElement, "contentEditable", "true");
      this.el.nativeElement.focus();
      this.setValue(this.el.nativeElement.innerText);
    }
    

    Check to see if an edit was made

    Next, set the ‘contentEditable’ attribute to ‘true’. Then, give it focus and save the value of the inner text.

    After the user finishes editing, we’re going to compare the value of the inner text with the previous one to see if there are any changes. That way, we’ll send the data to the parent only if there is a difference and thus reduce the number of unnecessary trips to the database.

    checkValues(): Observable<EditUpdate> {
      const newValue = this.el.nativeElement.innerText;
    
      if (this.value !== newValue) {
        const edit: EditUpdate = {
        id: this.id,
        update: this.update,
        value: newValue
        }
        this.onChange.emit(edit);
      }
      
      this.reset();
      return this.onChange;
    }
    

    Exit Edit Mode

    Don’t forget to reset edit mode by removing the ‘editing’ class and setting ‘contentEditable’ to ‘false.’

    reset() {
      this.editing = false;
      this.renderer.removeClass(this.el.nativeElement, "editing");
      this.renderer.setAttribute(this.el.nativeElement, "contentEditable", "false");
    }
    

    The final product

    Here’s the code all together. Hope you enjoy! https://gist.github.com/yokoishioka/f746f261a86c91ffb2a4ffdeef18a096

  • Combine the power of Angular services and directives to detect screen size changes

    Combine the power of Angular services and directives to detect screen size changes

    I’ve read a lot about how awesome Angular services can be (and they’re right) and have seen a few on directives (there should be way more), but what has really been exciting me lately is how to combine the two!

    Problem to solve

    Detect when the screen size changes from mobile, tablet or desktop to automatically toggle between a navigation menu and hamburger menu.

    Solution requirements

    1. Request and receive the values from anywhere in my Angular workspace without duplicating code or worrying about dependencies.
    2. Request the values from only the components who care about them.
    3. Receive the values as either numbers or device sizes, such as small, medium and large.
    4. Receive the values only if they change.

    Why not just use a service?

    To separate concerns and to be reusable, I created a toggle menu component that doesn’t care what content is inserted and is used by a menu component that only cares about creating the content. If I were to just use a service, both components would have to listen to the service even when the service wasn’t being used and the service would have to care about which state the component is in rather than just focusing on passing values.

    Why use a directive?

    Think of directives as being able to supplement your components without having to create another component to do it. It’s like having a phantom component that can do everything a component can do but without requiring an actual instance of itself. By not including the HostListener in the service, only the element that has the directive will be listening for screen size changes.

    Role breakdown

    Directive: Listens for when the screen width changes and sends those values to the service.

    Service: Receives values from anyone who uses it, categorizes the number by screen size (if requested) and sends values to whoever subscribes to them.

    Component: Tells the directive when to start listening for the changes. Subscribes to the service to get back the values, specifying it only cares when the values change. Determines when to stop sending values.

    The Directive

    Attach the selector to the element you want to listen for window events. You’ll want to include a HostListener for when the window first gets loaded and then also one if the screen gets resized. Then, send the values to the service.

    device-screen-size.directive.ts

    import { DevicesService } from './devices.service';
    
    @Directive({
    selector: '[cesDeviceScreenSize]'
    })
    export class DeviceScreenSizeDirective {
    
    constructor(
      private devices: DevicesService
    ) { }
    
    @HostListener('window:load', ['$event']) onLoad(event) {
      this.sendSize(event.currentTarget.innerWidth);
    }
    
    @HostListener('window:resize', ['$event']) onResize(event) {
      this.sendSize(event.currentTarget.innerWidth);
    }
    
    sendSize(width: number) {
      this.devices.setSize(width);
    }
    }
    

    The Service

    Inside of your service, you’ll want to use an input that will take in the values and pass those values to a subject so that anyone can subscribe to them, even if they aren’t using the directive.

    devices.service.ts

    import { Injectable, Input } from '@angular/core';
    import { Subject, Observable } from 'rxjs';
    import { ScreenSize, DetectType } from './devices';
    
    @Injectable({
      providedIn: 'root'
    })
    
    export class DevicesService {
      screenSize: Subject<any> = new Subject();
      @Input() detectSize: DetectType;
    
      constructor() { }
    
      setSize(width: number): Observable<any> {
        if (this.detectSize === 'number') {
          this.screenSize.next(width);
        }
    
        else {
          if (width < ScreenSize.medium) {
            this.screenSize.next('small');
          }
    
          else if (width > ScreenSize.medium) {
            this.screenSize.next('medium');
          }
    
          if (width > ScreenSize.large) {
            this.screenSize.next('large');
            }
          }
    
      return this.screenSize;
      }
    }
    

    The Component

    For my use case, I created a menu toggle component that needs to detect when the screen size changes so that it knows whether or not to display the hamburger icon instead of the full navigation menu. I specified to the service that I want to listen for device sizes rather than the screen size width because I only care when the device changes.

    I recently discovered the distinctUntilChanged RxJS operator that will only pass the value if it’s changed! How cool is that?? No more useless values clogging up my stream.

    I don’t have to listen for when the device is large because nothing changes between medium and large. And medium always comes in between small and large.

    Don’t forget to unsubscribe from the subject to avoid memory leaks.

    toggle-menu.component.ts

    import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core';
    import { DevicesService } from 'projects/devices/src/public-api';
    import { distinctUntilChanged } from 'rxjs/operators';
    import { Subscription } from 'rxjs';
    
    @Component({
      selector: 'ces-menu-toggle',
      templateUrl: './menu-toggle.component.html',
      styleUrls: ['./menu-toggle.component.scss']
    })
    
    export class MenuToggleComponent implements OnInit, OnDestroy {
      @Input() showMenuButton: boolean = false;
      @Input() showMenuContent: boolean = false;
      @Input() menuTitle: string = "menu";
      allowToggle: boolean;
      sub: Subscription;
    
      constructor(
        private devices: DevicesService
      ) { }
    
      ngOnInit() {
        this.devices.detectSize = "type";
        this.sub =      this.devices.screenSize.pipe(distinctUntilChanged()).subscribe(size => {
        this.checkSize(size);
      })
     }
    
      ngOnDestroy() {
        this.sub.unsubscribe();
      }
    
      closeMenu() {
        this.showMenuButton = true;
        this.showMenuContent = false;
      }
      
      openMenu() {
        this.showMenuButton = false;
        this.showMenuContent = true;
      }
    
      checkSize(size: string) {
        if (size === 'medium') {
          this.allowToggle = false;
          this.openMenu();
        }
    
        else if (size === 'small') {
          this.allowToggle = true;
          this.closeMenu();
        }
      }
    
      toggleNav() {
        if (this.allowToggle) {
          this.showMenuButton = !this.showMenuButton;
          this.showMenuContent = !this.showMenuContent;
         }  
      }
    }
    

    I also posted the code to GitHub Gist for better readability. You can view the menu in action on my blog’s website. (Expand and shrink the window after you click the link.) Thanks for reading!