I recently have received an interesting task. To customize some form input/logic for a single customer. While this will require us to modify very core logic, I wonder if it is possible to have a better way to extend this customized input/logic without touching it?

Challenge

The main two challenge is,

  1. These customized inputs have too many options(Over 100K for all the inputs). It has performance issue with angularjs.

  2. Touch less core logic as possible.

Overall, here is final solution. Store options in window object and use decorator to modify core logic.

Detail

Massive Data

Let's start from the first one. At the beginning, I just put all the options into our angular UI form.
And it is "super" slow. It is too slow, then chrome will ask you if you want to exit page because it believe page is down....(Well, if you ask why I don't put it in API and query them when needed, it because it is just for one customer. also a lot of reason, budget, time, so we decide to do it only on UI. ) The issue is caused by deep copy and deep compare while angularjs doing state transition. I try to mininize it by adding "$" before options variable, which will stop angularjs do deep comparison. It works. However, not enough, angularjs still copy these 100K options and it isn't an easy tasks. The final solution is store options outside of angular. The way I do it, is using tampermnkey to inject options into window object. So angularjs won't do any deep copy and deep comparing.

Extend Angularjs

Service

Here comes the second challenge. How to make customize logic for without touching it. The reason we don't want to touch it because it is very core logic, and one of the most important function we have. Solution is decorator
Example

export default angular
   .module('app.customLogic', [newInputModule])
   .decorator('customLogicService', ($delegate) => {
   // Condition to not modify service
   if(!hasCustomizeOptionInWindowObject()) {
      // Return original service
      return $delegate
   }

   let originalSomeFunction = $delegate.SomeFunction;
   
   $delegate.someFunction = function (originalInputs) {
   // Override original function
   }

   let originalAnotherFunction = $delegate.anotherFunction;
   $delegate.anotherFunction = function (originalInputs) {
    originalAnotherFunction.apply($delegate, arguments)
   // Do some more after origin function is executed
   }
   
   return $delegate;
})

Note: There is a special naming syntax to follow. Doc
Note: Be careful to use fat arrow(=>) here. Fat arrow function don't have arguments as usual function. Doc

You can also do the same thing to controller and modify some of its stuff.

Template

This is the part not extend but replace. Currently I don't find a good way to extend template. The waty I used here is to put custom template into template cache. When controller initialed, it first look at template cahce to see if ther is a template, or itwill use default template.

There is no good place to place them, so I put them in one of my decorator.

Example

export default angular
   .module('app.customLogic', [newInputModule])
   .decorator('customLogicService', ($delegate) => {
   // Condition to not modify service
   if(!hasCustomizeOptionInWindowObject()) {
      // Return original service
      return $delegate
   }

   $templateCache.put('template.html', TEMPLATE_URL);
   
   let originalSomeFunction = $delegate.SomeFunction;
   
   $delegate.someFunction = function (originalInputs) {
   // Override original function
   }

   let originalAnotherFunction = $delegate.anotherFunction;
   $delegate.anotherFunction = function (originalInputs) {
    originalAnotherFunction.apply($delegate, arguments)
   // Do some more after origin function is executed
   }
   
   return $delegate;
})

In the controller/directory add this line

if(!$templateCache.get('template.html')) {
   $templateCache.put('template.html', TEMPLATE_URL);
}

and change the way you import tempate to

$template.get('template.html')

Conclusion

Unfortunately, I have to stop at this point, I was thinking if we can put all these decorator code into tampermonkey script as well. But guess I run out of my luck. Decorator can only be declare at config phase. Tempermonkey script is kind of running at run phase.

Reference: