5 May

Watching for changes with Reactive Forms in Angular4

Problem

On a recent project working with Angular 4 and reactive forms a need came up to allow child components ( which were driven off off their own child form groups) be be able to detect value changes to other child components. From these changes we needed to apply rules and detect validity of the values.

Now I know your thinking

So your first response is probably duh thats what valueChange subscriptions are for, and you are totally correct. What started to happen though was a huge duplication of code across many components for not only detecting changes (valueChanges) but also detecting the validity of those changes (statusChanges). What we needed was a consolidated way to detect changes and validity across the entire parent form group and be able to deliver those changes to any sub components.

Enter the watcher service

With the help of RXjs Observables we were able to provide a single watch point that any component in the entire application could subscribe to to get any changes from any component. This service would deliver not only value changes but validity status changes as well. GoodBye code duplication I’m staying dry in this code storm.

Service Code

import {Injectable} from '@angular/core';
import {AbstractControl, FormControl, FormGroup} from '@angular/forms';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {Observable} from 'rxjs/Observable';
import {Subscription} from "rxjs/Rx";

declare let _;

export interface FormGroupChange {
  path: string;
  name: string;
  control: FormControl;
}

/*
 Allows you to subscribe to value and status changes for fields in a FormGroup. Events are debounced to avoid too many
 simultaneous calls, and are emitted even when the validity of a field updates because of dependencies on other fields.

 Usage:

 let subscription = this.watchFormGroupService.watch(this.formGroup, ['personalInformation.firstName', 'personalInformation.lastName'])
   .subscribe((data: FormGroupChange) => {
      // ...
   });

 // Don't forget to unsubscribe! (eg. in ngOnDestroy)
 subscription.unsubscribe();
 */

@Injectable()
export class WatchFormGroupService {

  private MAX_CHECK_COUNT = 5;

    public watch(formGroup: FormGroup, paths: string[], debounce = 400): Observable<FormGroupChange> {
    return Observable.create(observer => {
      let internalSubs = [];
      paths.map(path => {
        let control = formGroup.root.get(path);
        if(!control) {
          let checkCount = 0
          // lets give angular some time to finalize the form groups and make them available. This happens due to race conditions with when different components can 
          // load and when watchers are setup from other components.
          let checkAgainInterval = setInterval(() => {
            let control = formGroup.root.get(path);
            if(control) {
              clearInterval(checkAgainInterval);
              let eventData: FormGroupChange = {
                path: path,
                name: _.last(path.split('.')),
                control: control
              };
              let subject = new BehaviorSubject(eventData);
              internalSubs.push(Observable.merge(...[control.valueChanges, control.statusChanges, subject]).debounceTime(debounce).map(data => {
                observer.next(eventData);
              }).subscribe());
            }
            if(checkCount >= this.MAX_CHECK_COUNT) {
              console.warn("NO WATCHER PATH MATCH", path);
              clearInterval(checkAgainInterval);
            }
            checkCount++;
          }, 1000);
        } else {
          let eventData: FormGroupChange = {
            path: path,
            name: _.last(path.split('.')),
            control: control
          };
          let subject = new BehaviorSubject(eventData);
          internalSubs.push(Observable.merge(...[control.valueChanges, control.statusChanges, subject]).debounceTime(debounce).map(data => {
            observer.next(eventData);
          }).subscribe());
        }
      });

      // Provide a way of canceling and disposing the interval resource
      return function unsubscribe() {
        _.forEach(internalSubs, sub => {
          sub.unsubscribe();
        });
      };
    });
  }

}

Now one caveat to this was we need to know which child component and form control to watch on. With no other real way around it we agreed that using the dot notation to a component and field as a string while is ‘hard coded’ was the best balance between ease of use and pseudo coupling of components. Coupling in this case i feel is loosely said, because if the component doesn’t exist in the view, but its subscribed to, nothing will be broadcasted anyway.

So how do we use this thing

In any of your components you simply inject the WatchFormGroupService and watch on an array of fields.

NOTE: registration must be in the AfterViewInit or later, so all the form groups have time to build and setup from the OnInit event. This is important because when listening across all components we do not know when the form group will be ready from initialization.

What is great about the WatchFormGroupService is that it can work with many first class form group citizens (meaning you can watch different un-related form groups from the same application).

 ngAfterViewInit() {
    this.watcherSubscription = this.watchFormGroupService.watch(this.formGroup, [
      'personalInformation.mailingAddress.address1',
      'personalInformation.mailingAddress.address2',
      'personalInformation.mailingAddress.address3',
      'personalInformation.mailingAddress.zipcode',
      'personalInformation.mailingAddress.state',
      'personalInformation.mailingAddress.city'
    ]).subscribe(data => {
      if(data.control.value.isValid){
         this.formData[data.name] = data.control.value;
         this.formGroup.get("childInformation")
           .get('my.address').get(data.name)
           .patchValue(data.control.value);
        }
      }
    });
  }

In this example we are listening for address changes in the personalInformation component and then updating "my" component with the new values, but only if the value coming in is valid. While there would be more logic around when to update the value this code at least shows what is possible with the WatchFormGroupService

Cleanup

One thing to call out as with using any RSjs Observable, make sure you clean up your subscriptions. In OnDestroy of the watching component unsubscribe from the watcher service

  ngOnDestroy() {
    this.watcherSubscription.unsubscribe();
  }

I want to give a shout out to Alex Brombal who I collaborated with on the concept. He was the lucky one who got to write the WatchFormGroupService.

Check this out in action on Plunkr

UPDATE

Since I first published this a couple changes have been made to the WatchFormGroupService. We needed a way for subscribers to get values on the initial subscribe from from the service. This would be in the case the formGroup was loaded from database data, and a valueChange event has not fired yet. To accomplish this we added in a BehaviorSubject and loaded it with the initial value from the formControl.

  public watch(formGroup: FormGroup, paths: string[], debounce = 400): Observable<FormGroupChange> {
    return Observable.merge(...paths.map(path => {

      let control = formGroup.root.get(path);
      if (!control) {
        console.warn("NO WATCHER PATH MATCH", path);
        return Observable.empty();
      }
      let eventData = {
        path: path,
        name: _.last(path.split('.')),
        control: control
      };
      let subject = new BehaviorSubject(eventData);
      return Observable.merge(...[control.valueChanges, control.statusChanges, subject]).debounceTime(debounce).map(data => eventData);
    }));
  }
1 Apr

Using ngDragula with ngPrime from Angular 4

Recently on a project I had the requirement to provided table row reordering. The challenge to that was getting access to the table rows that were created dynamically at runtime and within an ngPrime data table. To provide the drag and drop support I turned to ngDragula. This is a great plugin that provides plenty of events and html support. One thing it currently does not provide is the ability to override the container it uses for setting up the drag and drop. It currently only can use the element that the directive was placed on. As you can see with the use of 3rd party components placing the directive in the proper place can be a challenge.

To address these short comings in my project I created my own version of the ngDragula Directive. Lets walk through what you need to do to add support for drag and drop to your ngPrime data tables.

Creating your own ngDragula Directive

To add support for integration with ngPrime a couple of things need to happen. First we need to override the container that dragula will attach to. In regards to drag and drop with tables the container needs to be the closest parent to the items you want to drag. In our case that ends up being the tbody element. Unfortunately ngPrime data tables do not allow access to this element, so we have no way to place the dragula directive in the correct location. Secondly we need to allow for delayed binding to the data table. Since we provide a collection of rows to the ngPrime data table the tbody tag will not be available onInit of the component. To make this work we need to bind the ngDragula directive after the view is available from Angular. Luckily Angular provides us with the AfterViewInit lifecycle hook. Lets take a look at the code.

The first thing we need to do is create our own version of the ngDragula directive. ensure you give it a unique name that wont clash with existing libs. Also add AfterViewInit to the implements of the class

import { Directive, OnChanges, AfterViewInit, OnInit, Input, ElementRef, SimpleChange } from '@angular/core';
import { DragulaService, dragula } from 'ng2-dragula';

@Directive({ selector: '[primeDragula]' })
export class PrimeDragulaDirective implements OnChanges, OnInit, AfterViewInit { }

Next we change the container to protected so we can provide extension later

   protected container: any;

Now we can implement the Angular events OnInit, AfterViewInit. In OnInit we wire up the options, and set the initial container element. New options that can be provided to the directive are ‘initAfterView’ and ‘childContainerSelector’. Here we check to see if late binding is needed and if not initialize the directive like usual. If late binding is needed AfterViewInit handles that check.

ngOnInit(){
    this.options = Object.assign({}, this.dragulaOptions);
    this.container = this.el.nativeElement;

    if(!this.options.initAfterView){
      this.initialize();
    }
  }

  ngAfterViewInit() {
    if(this.options.initAfterView){
      this.initialize();
    }
  }

Lets move the initialization code to its own method for reuse. Here the only new code is the ‘childContainerSelector’ check this is what gives us the ability to use a different container then the one the ngDragula directive was placed on. Notice how we set the mirrorContainer as well. This is because since the container is a sub element of the parent we want to ensure mirror object (the drag object visual that shows the movement) is positioned relative to the correct parent.

NOTE: an upgrade to this directive would be to use another property to dictate overriding the defauly mirrorContainer = ‘document.body’

 protected initialize(){    
    if(this.options.childContainerSelector){
        //find the element starting at the directive element and search down
        this.container = this.el.nativeElement.querySelector(this.options.childContainerSelector);
        this.options.mirrorContainer = this.container;
      }

    let bag = this.dragulaService.find(this.primeDragula);
    let checkModel = () => {
      if (this.dragulaModel) {
        if (this.drake.models) {
          this.drake.models.push(this.dragulaModel);
        } else {
          this.drake.models = [this.dragulaModel];
        }
      }
    };
    if (bag) {
      this.drake = bag.drake;
      checkModel();
      this.drake.containers.push(this.container);
    } else {
      this.drake = dragula([this.container], this.options);
      checkModel();
      this.dragulaService.add(this.primeDragula, this.drake);
    }
  }

Finally, add in the pre-existing OnChanges method from the ngDragula directive

public ngOnChanges(changes: { dragulaModel?: SimpleChange }): void {
    if (changes && changes.dragulaModel) {
      if (this.drake) {
        if (this.drake.models) {
          let modelIndex = this.drake.models.indexOf(changes.dragulaModel.previousValue);
          this.drake.models.splice(modelIndex, 1, changes.dragulaModel.currentValue);
        } else {
          this.drake.models = [changes.dragulaModel.currentValue];
        }
      }
    }
  }

Using the new directive

So now the directive is create we are ready to implement it.

In our component template where we define our ngPrime data table lets add the dragula directive.

 <p-dataTable [value]="rows" [primeDragula]="bag" [dragulaModel]="rows" 
  [dragulaOptions]="{ childContainerSelector: 'tbody', initAfterView: true }">
  <p-column header="Move">
    <ng-template pTemplate="body" let-rowData="rowData">
      <i class="fa fa-bars"></i>
    </ng-template>
  </p-column>
  <p-column field="name" header="Name">          
  </p-column>      
</p-dataTable>

That is all there is to it. Pretty simple changes that allow a greater user experience.

Gotcha’s:

  • Dont forget to add the javascript dragula version to your package.json and angular CLI styles and scripts sections. This is a requirement for ngDragula to work as expected.

Versions used

"@angular/animations": "4.0.0",
"@angular/common": "4.0.0",
"@angular/compiler": "4.0.0",
"@angular/core": "4.0.0",
"@angular/forms": "4.0.0",
"@angular/http": "4.0.0",
"@angular/platform-browser": "4.0.0",
"@angular/platform-browser-dynamic": "4.0.0",
"@angular/router": "4.0.0",
"dragula": "^3.7.2",
"lodash": "4.17.4",
"ng2-dragula": "^1.3.0",
"primeng": "2.0.5",
"rxjs": "5.1.0",
"zone.js": "0.8.4"

Hopefully you found this helpful. You can see a working example here on Plunkr. Also these changes have been submitted for review to the guys over at valor-software. With a little luck I can just use the official version of ngDragula one day!

10 Jul

Sticky Validation – Angular 1.5.7

Why was this created

Sticky validation is meant to fill the gap in Angular when you only want to show errors on submit,

and leave those errors displayed even after the user has started changing the input that had the errors.

Typically in Angular they operate on the concept of dynamic validation.

This validation evaluates the model in real time and adjusts the error messages accordingly based on user input.

Once the user has typed something else the original error that caused the problem has been removed automatically by Angular (provided they corrected that original error)

and the model value change has either became valid or possibly still invalid with another validation error occurring.

With the current implementation of Angular 1.5.7 there was no way to keep the original error and invalid state set once the user fixed

the problem. From a UX perspective consistency is vital and since we want to operate in the context of only show errors on submit,

the existing errors should be visible until a submit was again performed.

What is sticky validation

Sticky validation allows you to work with the pre-existing ng-messages/ng-message directives provided by Angular.

This injects new properties on the form and $error objects to allow for a constant state to be available when displaying error messages

to the user. Sticky validation also operates on the premise that you want to implement "submit" only validation (although nothing is prohibiting sticky state properties from being used with regular validation).

Sticky validation will allow you to preserve the original error message displayed and have a consistent property available indicating

that "submits" are happening (currently in Angular if you try and use $submitted and $invalid together to show the messages, once $invalid becomes false the messages will hide).

How to implement

Implementing can be done in 3 easy steps, once the directive has been added to your project and registered with your Angular app.

  1. Replace any existing ng-submit with se-submit

    <form name="cntrl.form" se-submit="cntrl.submit()" novalidate>
    

  2. To ensure ng-messages only display when the form is submitted and invalid. Add the following in the ng-if.

    $fieldInvalid is the custom property added to each element that is kept in sync with Angulars’ $invalid property

    <div ng-messages="cntrl.form.badgenumber.$error" ng-if="cntrl.form.$submitted && cntrl.form.badgenumber.$fieldInvalid">
    
        all the ng-message directives
    
    </div>        
    

  3. In your ng-message elements switch the current validation name “required” (Angular implementation) with “stickyrequired”

    <div ng-message="stickyrequired" class="has-error">
    
        Badge Number must be provided
    
    </div>        
    

Pitfalls

Currently this directive only works for a single level form. If your form has child forms nested within, this will NOT inject

the custom properties. I have not come across many cases where usage of sub forms is needed, but there are always outliers.

If child form support is needed please feel free to submit a pull request for review.

Example Walkthrough

  1. User comes to page and sees the form to fill out

    alt text

  2. User types invalid characters into the form field and clicks “Continue”

    alt text

    The error is displayed in standard Angular fashion

  3. With use of sticky validation, once the field is cleared (which sets its state back to valid). the error message and error highlighting remain intact

    alt text

  4. User corrects the issue and clicks “Continue” again. That validation issue is then re-evaluated and cleared accordingly.

    alt text

Final Words

I hope this helps in overcoming the pitfalls and gaps with Angular validation when it comes to not wanting dynamic validation on all the time.
One of the great things about this is it still allows you to use Angulars’ built in validation for dynamic error messaging. Want the source code? Visit my GitHub

For more information view

ng-messages

angular forms

9 Jul

Scroll watcher directive for Angular 1.5.7

Here is a directive I came up with to help with keeping track of page scroll position and when scrolling has started and stopped. I had a need for this in trying to hide page content while the user was scrolling up/down a page, and then re-showing the content once the scrolling had stopped. Currently this is only setup up to work at the document level, but and easy modification could be made to allow a new property to drive what scroll area is being monitored. I hope this helps others in case they need a way to tell if page scrolling has started or stopped.

How to Implement

HTML
Simply add the directive to the page you want to monitor scrolling on. Next add the scroll-callback function you want to be called from directive when scrolling starts and stops

<div page-scroll-watcher scroll-callback="cntl.scrollStop($event, isEndEvent, isScrollingEvent)">

Callback Function
Note: sample code is in ES6 format. This is an excerpt from a angular controller

 //$event is the standard scroll event from the browser. This contains the X,Y information
 //isEndEvent signals when scrolling has stopped
 //isScrollingEvent signals when scrolling has started
 scrollStop($event, isEndEvent, isScrollingEvent) {
    if (isEndEvent) {
      this.showBottomBar = true;
      return;
    }
    if(isScrollingEvent)
    {
      this.showBottomBar = false;
      return;
    }
  }

Now that we have the how to implement lets get to the good stuff. The code that makes this all work
Page Scroll Directive
Note: code is in ES6 format


//this would just need to be registered with your Angular app
import angular from "angular";
import * as _ from "lodash";

const directivesModule = angular.module("MyDirectives", [])
  .directive("pageScrollWatcher", ["$window", "$document", pageScrollWatcher]);

function pageScrollWatcher($document) {
  return {
    restrict: "A",
    scope: {
      scrollCallback: "&"
    },
    link: function (scope) {
      //here could be updated to use the element this directive is attached to if needed to watch a scrollable div container
      const el = angular.element($document); 

      //here we delay evaluating the scrolling events until they have stopped
      const dbnce = _.debounce(function (e) {
        //send event that scrolling stopped
        scope.$apply(function () {
          //execute the provided callback
          scope.scrollCallback({ $event: e, isEndEvent: true, isScrollingEvent: false });
        });

        //register first scroll interceptor. Since scrolling has stopped we now need to register a start scrolling event binding
        el.bind("scroll", firstScrollFunc);

      }, 200);

      const firstScrollFunc = function (e) {
        //so we have detected the scrolling needs to start. Since this is a one time event between starts/stops we need to
        //unregister the start scrolling event
        el.unbind("scroll", firstScrollFunc);
        scope.$apply(function () {
          //execute the provided callback
          scope.scrollCallback({ $event: e, isEndEvent: false, isScrollingEvent: true });
          //We do this incase angular removes dom parts causing the scroll bar to disappear or change.
          //we need to trigger the end event again 
          dbnce(e);
        });
      };

      //on first load of directive register the start and stop events
      el.bind("scroll", firstScrollFunc);
      el.bind("scroll", dbnce);

      scope.$on("$destroy", function handleDestroyEvent() {
        //when switching pages remove event
        el.unbind("scroll", dbnce);
        el.unbind("scroll", firstScrollFunc);
      });

    }
  };
}

Want the source? Visit my GitHub

7 Jan

Bookmarklets

 

Creating a bookmarklet

    • In Google Chrome:
        1. Click on the three-dot menu icon in the top-right corner.
        1. Hover over “Bookmarks” and select “Bookmark manager.”
        1. Right-click on a folder (or the bookmarks bar) where you want to save the bookmarklet.
        1. Choose “Add bookmark.”
        1. In the “Name” field, give your bookmarklet a descriptive name.
        1. In the “URL” or “Address” field, paste the JavaScript code for your bookmarklet. Make sure it starts with `javascript:`.
        1. Click “Save.”
    • In Mozilla Firefox:
        1. Click on the three-line menu icon in the top-right corner.
        1. Choose “Bookmarks” and then “Show All Bookmarks” to open the Library.
        1. Right-click on a folder (or the bookmarks toolbar) where you want to save the bookmarklet.
        1. Select “New Bookmark.”
        1. Give your bookmarklet a name in the “Name” field.
        1. In the “Location” field, paste the JavaScript code for your bookmarklet, starting with `javascript:`.
        1. Click “Add.”
    • In Microsoft Edge (Chromium-based):
        1. Click on the three-dot menu icon in the top-right corner.
        1. Choose “Favorites” and then “Manage favorites.”
        1. Right-click on a folder (or the favorites bar) where you want to save the bookmarklet.
        1. Select “Add a favorite.”
        1. Give your bookmarklet a name.
        1. In the “URL” field, paste the JavaScript code for your bookmarklet, starting with `javascript:`.
        1. Click “Save.”

VSTS/TFS Helpers

TFS Popout Editor (older version of Visual Studio Online)

javascript:(function(){if($===window.jQuery){var mainCon = $('div[rawtitle=\'Acceptance Criteria\']');mainCon.css({ position:'fixed', 'z-index':30000, top: '40px', left: '50px', height: '800px', 'background-color': '#ccc', width: '1500px'});mainCon.find('.richeditor-container').css({height: '750px'});}})()

TFS Done Editing (older version of Visual Studio Online)

javascript:(function(){if($===window.jQuery){var mainCon = $('div[rawtitle=\'Acceptance Criteria\']');mainCon.css({ position:'inherit', 'z-index':0, top: '', left: '', height: '', 'background-color': '', width: ''});mainCon.find('.richeditor-container').css({height: '250px'});}})()

Find My Items (old azure portal)

(update the href in the bookmark and replace XXX with your search filter)

javascript:(function(){$('.fx-grid-searchbox > input').val('XXX').blur();})()

Other Helpers

Add Skips to Rumble

javascript: (function() {    function skip(value) {        var videos = document.getElementsByTagName("video");        for (var i = 0; i < videos.length; i++) {            var video = videos[i];            video.currentTime += value;        }    }    function clean(className) {        var elementToDelete = document.getElementsByClassName(className);        for (var i = 0; i < elementToDelete.length; i++) {            var overlay = elementToDelete[i];            overlay.parentNode.removeChild(overlay);        }    }    function addOverlay(videoObj, skipAmount, align) {        var thisClass = "videoSkip_" + align;        var parentDiv = videoObj.parentNode;        if (!parentDiv) {            return;        }        var overlayDiv = document.createElement("div");        overlayDiv.classList.add(thisClass);        overlayDiv.style.position = "absolute";        overlayDiv.style.width = "80px";        overlayDiv.style.height = "100%";        overlayDiv.style.zIndex = "9999";        overlayDiv.style.backgroundColor = "rgba(0, 0, 0, 0.1)";        overlayDiv.style[align] = 0;        overlayDiv.style.bottom = "50px";        overlayDiv.title = "Skip " + skipAmount + " seconds";        parentDiv.appendChild(overlayDiv);        overlayDiv.onmouseover = function() {            overlayDiv.style.cursor = "pointer";        };        overlayDiv.addEventListener("click", function() {            skip(skipAmount);        });        parentDiv.insertBefore(overlayDiv, parentDiv.firstChild);    }    function addClickSkip(skipAmount, align) {        var videos = document.getElementsByTagName("video");        for (var i = 0; i < videos.length; i++) {            var video = videos[i];            addOverlay(video, skipAmount, align);        }    }    clean("videoSkip_left");    clean("videoSkip_right");    addClickSkip(-10, "left");    addClickSkip(10, "right");})();

To use this on a mobile device follow this guide

Add Skips to Youtube

javascript: (function() {    function skip(value) {        var videos = document.getElementsByTagName("video");        for (var i = 0; i < videos.length; i++) {            var video = videos[i];            video.currentTime += value;        }    }    function clean(className) {        var elementToDelete = document.getElementsByClassName(className);        for (var i = 0; i < elementToDelete.length; i++) {            var overlay = elementToDelete[i];            overlay.parentNode.removeChild(overlay);        }    }    function addOverlay(videoObj, skipAmount, align) {        var thisClass = "videoSkip_" + align;        var parentDiv = videoObj.parentNode;        if (!parentDiv) {            return;        }        var overlayDiv = document.createElement("div");        overlayDiv.classList.add(thisClass);        overlayDiv.style.position = "absolute";        overlayDiv.style.width = "80px";        overlayDiv.style.height = "100vh";        overlayDiv.style.zIndex = "9999";        overlayDiv.style.backgroundColor = "rgba(0, 0, 0, 0.1)";        overlayDiv.style[align] = 0;               overlayDiv.title = "Skip " + skipAmount + " seconds";        parentDiv.appendChild(overlayDiv);        overlayDiv.onmouseover = function() {            overlayDiv.style.cursor = "pointer";        };        overlayDiv.addEventListener("click", function(e) {   e.stopPropagation();        skip(skipAmount);        });        parentDiv.insertBefore(overlayDiv, parentDiv.firstChild);    }    function addClickSkip(skipAmount, align) {        var videos = document.getElementsByTagName("video");        for (var i = 0; i < videos.length; i++) {            var video = videos[i];            addOverlay(video, skipAmount, align);        }    }    clean("videoSkip_left");    clean("videoSkip_right");    addClickSkip(-10, "left");    addClickSkip(10, "right");})();

To use this on a mobile device follow this guide

Dark Mode

javascript: function addcss(css){ var head = document.getElementsByTagName('head')[0]; var s = document.createElement('style');      s.setAttribute('type', 'text/css'); s.appendChild(document.createTextNode(css));      head.appendChild(s);  }  addcss('html{filter: invert(1) hue-rotate(180deg)}');  addcss('img{filter: invert(1) hue-rotate(180deg)}');  addcss('video{filter: invert(1) hue-rotate(180deg)}')

Thanks to Sahil Malik

Expand GIT Code Viewer

javascript:(function(){ document.querySelector('.blob-wrapper').style.width = 'max-content'; })()

Expand AWS S3 Column

javascript:(function(){ Object.values(document.getElementsByClassName('truncate')).forEach(function(e){ e.classList.remove('truncate');}) })()

Display Local Storage Size (in console)

javascript:var total = 0;for(var x in localStorage) { var amount = (localStorage[x].length * 2) / 1024 / 1024; total += amount; console.log( x + '=' + amount.toFixed(2) + ' MB');}console.log( 'Total: ' + total.toFixed(2) + ' MB');

Display Window Size

javascript:(function(){var f=document,a=window,b=f.createElement('div'),c='position:fixed;top:0;left:0;color:#fff;background:#222;padding:5px 1em;font:14px sans-serif;z-index:999999',e=function(){if(a.innerWidth===undefined){b.innerText=f.documentElement.clientWidth+'x'+f.documentElement.clientHeight;}else if(f.all){b.innerText=a.innerWidth+'x'+a.innerHeight;}else{b.textContent=window.innerWidth+'x'+window.innerHeight;}};f.body.appendChild(b);if(typeof b.style.cssText!=='undefined'){b.style.cssText=c;}else{b.setAttribute('style',c);}e();if(a.addEventListener){a.addEventListener('resize',e,false);}else{if(a.attachEvent){a.attachEvent('onresize',e);}else{a.onresize=e;}}})();

Expand dev.azure.com Pipelines Release Names

javascript:(function(){ Object.values(document.getElementsByClassName('overflow-ellipsis')).forEach(function(e){ $(e).css('white-space', 'break-spaces'); }) })();

Auto Close Zoom On Participant Count

javascript:(function(){var retVal = prompt('Enter the number of participants to leave meeting on: ', '10'); var ivl = setInterval(function(){ var currentAmount = document.getElementsByClassName('footer-button__number-counter')[0]; var btn = document.getElementsByClassName('footer__leave-btn')[0]; if(!btn || !currentAmount){ alert('Page has changed plugin not available'); return; } console.log('Checking participant count', currentAmount.innerText); if(retVal === currentAmount.innerText){ clearInterval(ivl); console.log('Leaving meeting'); btn.click(); setTimeout(function(){ var confirmLeave = document.getElementsByClassName('leave-meeting-options__btn')[0]; if(!confirmLeave){ window.close(); } confirmLeave.click(); }, 2000); } }, 15000);})();

Fix SonarLint Display to show Rule ID

javascript:(function(){document.querySelectorAll('.coding-rule').forEach((ruleTitle)=>{const rule=ruleTitle.getAttribute('data-rule');if(rule){const code=rule.split(":")[1];const anchor=ruleTitle.querySelector('a');if(anchor){const hrefValue=anchor.getAttribute('href');const text=anchor.textContent;var ruleString="Rule: "+code+" ";var b=document.createElement('b');b.innerText=ruleString;var span=document.createElement('span');span.innerText=text.replace(ruleString,"");anchor.innerText="";anchor.innerHtml="";anchor.appendChild(b);anchor.appendChild(span);}}});})();

Mark all comments resolved in Azure Dev Ops Code Review

javascript:(function(){const e=document.getElementsByTagName("button");for(let n=0;n<e.length;n++){const o=e[n];o.textContent.includes("Resolve")&&o.click()}})();

Open link in Archive.Today

javascript:void(open('https://archive.today/?run=1&url=%27+encodeURIComponent(document.location)))

AI Helpers

ChatGTP Widen Results Area

javascript:(function(){function insertCustomStyle(cssText){var style=document.createElement('style');style.textContent=cssText;document.head.appendChild(style);}var customCss='.flex .flex-1 .text-base { margin: 5px !important; max-width: 100%; }';insertCustomStyle(customCss);})();

Bard Widen Results Area

javascript:(function(){function insertCustomStyle(cssText){var style=document.createElement('style');style.textContent=cssText;document.head.appendChild(style);}var customCss='.conversation-container { max-width: 100%; }';insertCustomStyle(customCss);})();

Insert AI Prompt (Works with ChatGTP and Bard websites)

Based on This One Prompt Will 10X Your Chat GPT Results (This is outdated refer to ChatGTP 3.5 Instructions for a better example prompt)

To use place cursor in input box for website. Click bookmarklet to insert prompt. Press enter.

For a list of commands after prompt is executed type /help

javascript: (function() {  document.trusted=true;  var textToCopy = `Upon starting our interaction, auto run these Default Commands throughout our entire conversation. Refer to Appendix for command library and instructions: /role_play "Expert ChatGPT Prompt Engineer" /role_play "infinite subject matter expert" /auto_continue "♻%EF%B8%8F": ChatGPT, when the output exceeds character limits, automatically continue writing and inform the user by placing the ♻%EF%B8%8F emoji at the beginning of each new part. This way, the user knows the output is continuing without having to type "continue". /periodic_review "🧐" (use as an indicator that ChatGPT has conducted a periodic review of the entire conversation. Only show 🧐 in a response or a question you are asking, not on its own.) /contextual_indicator "🧠" /expert_address "🔍" (Use the emoji associated with a specific expert to indicate you are asking a question directly to that expert) /chain_of_thought/custom_steps /auto_suggest "💡": ChatGPT, during our interaction, you will automatically suggest helpful commands when appropriate, using the 💡 emoji as an indicator. Priming Prompt:You are an Expert level ChatGPT Prompt Engineer with expertise in all subject matters. Throughout our interaction, you will refer to me as "Obi-Wan". 🧠 Let's collaborate to create the best possible ChatGPT response to a prompt I provide, with the following steps:1. I will inform you how you can assist me.2. You will /suggest_roles based on my requirements.3. You will /adopt_roles if I agree or /modify_roles if I disagree.4. You will confirm your active expert roles and outline the skills under each role. /modify_roles if needed. Randomly assign emojis to the involved expert roles.5. You will ask, "How can I help with {my answer to step 1}?" (💬)6. I will provide my answer. (💬)7. You will ask me for /reference_sources {Number}, if needed and how I would like the reference to be used to accomplish my desired output.8. I will provide reference sources if needed9. You will request more details about my desired output based on my answers in step 1, 2 and 8, in a list format to fully understand my expectations.10. I will provide answers to your questions. (💬)11. You will then /generate_prompt based on confirmed expert roles, my answers to step 1, 2, 8, and additional details.12. You will present the new prompt and ask for my feedback, including the emojis of the contributing expert roles.13. You will /revise_prompt if needed or /execute_prompt if I am satisfied (you can also run a sandbox simulation of the prompt with /execute_new_prompt command to test and debug), including the emojis of the contributing expert roles.14. Upon completing the response, ask if I require any changes, including the emojis of the contributing expert roles. Repeat steps 10-14 until I am content with the prompt.If you fully understand your assignment, respond with, "How may I help you today, {Name}? (🧠)"Appendix: Commands, Examples, and References1. /adopt_roles: Adopt suggested roles if the user agrees.2. /auto_continue: Automatically continues the response when the output limit is reached. Example: /auto_continue3. /chain_of_thought: Guides the AI to break down complex queries into a series of interconnected prompts. Example: /chain_of_thought4. /contextual_indicator: Provides a visual indicator (e.g., brain emoji) to signal that ChatGPT is aware of the conversation\%27s context. Example: /contextual_indicator 🧠5. /creative N: Specifies the level of creativity (1-10) to be added to the prompt. Example: /creative 86. /custom_steps: Use a custom set of steps for the interaction, as outlined in the prompt.7. /detailed N: Specifies the level of detail (1-10) to be added to the prompt. Example: /detailed 78. /do_not_execute: Instructs ChatGPT not to execute the reference source as if it is a prompt. Example: /do_not_execute9. /example: Provides an example that will be used to inspire a rewrite of the prompt. Example: /example "Imagine a calm and peaceful mountain landscape"10. /excise "text_to_remove" "replacement_text": Replaces a specific text with another idea. Example: /excise "raining cats and dogs" "heavy rain"11. /execute_new_prompt: Runs a sandbox test to simulate the execution of the new prompt, providing a step-by-step example through completion.12. /execute_prompt: Execute the provided prompt as all confirmed expert roles and produce the output.13. /expert_address "🔍": Use the emoji associated with a specific expert to indicate you are asking a question directly to that expert. Example: /expert_address "🔍"14. /factual: Indicates that ChatGPT should only optimize the descriptive words, formatting, sequencing, and logic of the reference source when rewriting. Example: /factual15. /feedback: Provides feedback that will be used to rewrite the prompt. Example: /feedback "Please use more vivid descriptions"16. /few_shot N: Provides guidance on few-shot prompting with a specified number of examples. Example: /few_shot 317. /formalize N: Specifies the level of formality (1-10) to be added to the prompt. Example: /formalize 618. /generalize: Broadens the prompt\%27s applicability to a wider range of situations. Example: /generalize19. /generate_prompt: Generate a new ChatGPT prompt based on user input and confirmed expert roles.20. /help: Shows a list of available commands, including this statement before the list of commands, “To toggle any command during our interaction, simply use the following syntax: /toggle_command "command_name": Toggle the specified command on or off during the interaction. Example: /toggle_command "auto_suggest"”.21. /interdisciplinary "field": Integrates subject matter expertise from specified fields like psychology, sociology, or linguistics. Example: /interdisciplinary "psychology"22. /modify_roles: Modify roles based on user feedback.23. /periodic_review: Instructs ChatGPT to periodically revisit the conversation for context preservation every two responses it gives. You can set the frequency higher or lower by calling the command and changing the frequency, for example: /periodic_review every 5 responses24. /perspective "reader\%27s view": Specifies in what perspective the output should be written. Example: /perspective "first person"25. /possibilities N: Generates N distinct rewrites of the prompt. Example: /possibilities 326. /reference_source N: Indicates the source that ChatGPT should use as reference only, where N = the reference source number. Example: /reference_source 2: {text}27. /revise_prompt: Revise the generated prompt based on user feedback.28. /role_play "role": Instructs the AI to adopt a specific role, such as consultant, historian, or scientist. Example: /role_play "historian" 29. /show_expert_roles: Displays the current expert roles that are active in the conversation, along with their respective emoji indicators.Example usage: Obi-Wan: "/show_expert_roles" Assistant: "The currently active expert roles are:1. Expert ChatGPT Prompt Engineer 🧠2. Math Expert 📐"30. /suggest_roles: Suggest additional expert roles based on user requirements.31. /auto_suggest "💡": ChatGPT, during our interaction, you will automatically suggest helpful commands or user options when appropriate, using the 💡 emoji as an indicator. 31. /topic_pool: Suggests associated pools of knowledge or topics that can be incorporated in crafting prompts. Example: /topic_pool32. /unknown_data: Indicates that the reference source contains data that ChatGPT doesn\%27t know and it must be preserved and rewritten in its entirety. Example: /unknown_data33. /version "ChatGPT-N front-end or ChatGPT API": Indicates what ChatGPT model the rewritten prompt should be optimized for, including formatting and structure most suitable for the requested model. Example: /version "ChatGPT-4 front-end"Testing Commands:/simulate "item_to_simulate": This command allows users to prompt ChatGPT to run a simulation of a prompt, command, code, etc. ChatGPT will take on the role of the user to simulate a user interaction, enabling a sandbox test of the outcome or output before committing to any changes. This helps users ensure the desired result is achieved before ChatGPT provides the final, complete output. Example: /simulate "prompt: \%27Describe the benefits of exercise.\%27"/report: This command generates a detailed report of the simulation, including the following information:• Commands active during the simulation• User and expert contribution statistics• Auto-suggested commands that were used• Duration of the simulation• Number of revisions made• Key insights or takeawaysThe report provides users with valuable data to analyze the simulation process and optimize future interactions. Example: /reportHow to turn commands on and off:To toggle any command during our interaction, simply use the following syntax: /toggle_command "command_name": Toggle the specified command on or off during the interaction. Example: /toggle_command "auto_suggest"`;    console.log(textToCopy);    var activeElement = document.activeElement;    if (activeElement && activeElement.tagName.toLowerCase() === "input" || activeElement.tagName.toLowerCase() === "textarea") {        activeElement.value = textToCopy;  return;   }    if (activeElement && activeElement.tagName.toLowerCase() === "div" || activeElement.tagName.toLowerCase() === "p") {    activeElement.innerText = textToCopy; return;   }  })();

Thanks to codewithbernard