Note: You are currently browsing the documentation for an older version of the library.
Click here to switch to the newest version.Trivia
How it works
This library doesnāt rely on any special āhacksā to load the dynamic components. Most notably, it uses ComponentFactory.create() from Angularās public api, which is safe and has been around since Angular 2.
It then adds a lot of custom code around this core function to render the components at exactly the right place, register inputs and outputs, project the content properly, activate change detection, update and destroy them automatically, etc. - all to integrate the dynamic components into Angular as naturally as possible.
If you are curious about the inner workings of the library, hereās a short description:
- A content string is passed as @Input() to the
OutletComponent
and an array of parsers is retrieved either as another @Input() or from the global settings. - The
findHooks()
-method of all registered parsers is called and (with the help of the returnedHookPosition[]
) all found hooks are replaced with placeholders. - The content string is then parsed by the native browser HTML parser to create a DOM tree, which is then inserted as the innerHTML of the
OutletComponent
. - For each found hook, the
loadComponent()
-method of its parser is called to get the component class. The placeholder elements are replaced with the final component host elements and the components dynamically loaded viaComponentFactory.create()
. - For each created component, the
getBindings()
-method of its parser is called and the returned inputs/outputs passed to and subscribed with the component. - On future update requests (by default, on every change detection run),
getBindings()
is called again to see if it returns different values than before (for example, if the bindings are generated from data that has since changed). If so, the components will be updated accordingly. - When the
OutletComponent
is destroyed, all dynamically-loaded components are destroyed as well.
Security
One of the goals of this library was to make it safe to use even with potentially unsafe input, such as user-generated content. It is also designed to grant developers maximum control over which components are allowed to be loaded, and how. It uses the following techniques to achieve this:
Most notably, it uses Angularās DOMSanitizer
by default to remove all unsafe HTML, CSS and JS in the content string that is not part of a hook. Though not recommended, you may turn this setting off in the OutletOptions. You will then have to ensure yourself that the rendered content does not include Cross Site Scripting attacks (XSS) or other malicious code, however.
Excluded from being checked by the DOMSanitizer
are the component hooks themselves (as it may remove them depending on their pattern). It is therefore the hook parserās responsibility to ensure that whatever malicious code there may be in the hook is not somehow transferred to the rendered component. For this reason, the standard SelectorHookParser
that comes with this library does not rely on JavaScriptās dangerous eval()
function to evaluate inputs and outputs and instead internally uses JSON.parse()
to safely turn strings into variables. Ensure that when writing custom parsers for hooks that take their inputs/outputs directly from the text, similar security precautions are taken!
In addition to this, the scope of code that is accessible by the author of the content string is limited to the context object, which you can customize to your liking.
Finally, which components/hooks can be used by the author can be freely adjusted for each OutletComponent
, as can their allowed inputs/outputs.
Caveats
- As this library does not parse the content string as an actual Angular template, template syntax such as
*ngIf
,*ngFor
, attribute bindings[style.width]="'100px'"
, interpolation `` etc. will not work! This functionality is not planned to be added either, as it would require a fundamentally different approach by relying on the JiT template compiler (which this library intentionally doesnāt) or even creating a custom Angular template parser. - Hooks can only load components, not directives. Thereās no way to dynamically create directives as far as iām aware. If you want to load a directive into the content string, try loading a component that contains that directive instead.
- Accessing
@ContentChildren
does not work in dynamically-loaded components, as these have to be known at compile-time. However, you can still access them via onDynamicMount().
Comparison with similar libraries
Angular elements
Angular elements allows you to register custom HTML elements (like component selector elements) with the browser itself that automatically load and host an Angular component when they appear anywhere in the DOM (see Web components) - even outside of the Angular app. For that reason, these elements work in dynamic content as well and may satisfy your needs.
However, there are a number of advantages this library offers compared to Angular elements:
- Pattern flexibility: You are not limited to load components by unique HTML elements. A hook can have any form, be any element or even consist of just text (see āemojiā example or ālinkā example).
- Scope: When using Angular elements, web components will automatically load from anywhere in the DOM as they are globally registered with the browser. With this library, you can to specify on each
OutletComponent
individually which exact components to look for. - Communication: In Angular elements, there is no direct line of communication between the parent component rendering the dynamic content and dynamic components loaded as children (such as the context object from this library). You will have to fallback on services to transfer data.
- Bindings: Though Angular elements allows passing static inputs as HTML attributes to components, it doesnāt parse them. This means that all inputs are strings by default and you will have to manually turn them into booleans, arrays, objects etc. yourself. This library parses them automatically for you, much like a normal Angular template - in addition to accepting actual variables from the context object as well.
- Projected content: Angular elements doesnāt normally render projected content in the componentās
<ng-content>
. There is a workaround involving<slot>
, but its not ideal. This library renders<ng-content>
normally.
Ng-Dynamic
Ng-Dynamic was one of the inspirations for this library and is unfortunately not maintained anymore. It consited of two parts, but Iāll just focus on its <dynamic-html>
-component, which worked like a simpler version of this library. In short, it looked for a component selector in a content string and simply replaced it with the corresponding component, also using ComponentFactory.create()
. As that is pretty much all it focused on, it:
- required selector elements to load components (hooks can be anything)
- provided no direct line of communication to the parent component like the context object
- did not automatically handle inputs/outputs in any way
- did not automatically handle projected content in any way
- had no security features whatsoever
- could not be customized through options
One can think of Angular Dynamic Hooks picking up the torch from ng-dynamicās <dynamic-html>
-component and taking it further.
Runtime compilation, Angular compile, etc.
There are also multiple libraries out there that render full Angular templates dynamically and rely on the JiT-compiler to do so. Many of them do not offer support for AoT-compilation (which Ivy uses by default). While it is technically possible to load the JiT-compiler during runtime in AoT-mode, it is quite hacky and may break without warning.
Also, note that rendering a dynamic template as though it were a static file is dangerous if you do not fully control the content, as all Angular components, directives or template syntax expressions are blindly executed just like in a static template.
Runtime compilation also suffers from most of the same drawbacks as the other libraries listed here, such as the lack of flexbility and control etc., so I wonāt list them seperately here.
Special thanks
Thanks to Ng-Dynamic for giving me the idea for this library (as well this blog post, which explains it more).
I am also grateful to Jesus Rodriguez & Ward Bell for their in-depth presentation on the topic.