One of the big (and mostly undocumented) shifts in Angular 2 is learning how to think about the DOM (Document Object Model) as an abstracted API. No longer can we be satisfied with just rendering in the browser - now our code needs to be compatible with WebWorkers, servers, and any other context for which people will write adapters (ex, NativeScript). This freedom, however, comes with a cost of complexity. And, relatively simple operations, like copying HTML, become, well, complex. Ideally, I'd like to be able to copy the innerHTML of a component in a platform-agnostic way; but, ultimately, the only way that I could figure out how to do it - without using native browser APIs - was to use the BrowserDomAdapter service.
Most of the time, in an AngularJS application, you only need to represent content once. But, in some use-cases, you need to take content and clone it. And, not cloned as a component instance but rather as a static copy of some portion of the component tree.
Take, for example, a drag-and-drop feature where the draggable item is represented as a "ghost" under the user's cursor. This ghost element is not an active component (meaning, it doesn't have interpolation bindings or host-event bindings), it's just a copy of the target element at the time the drag-operation was initiated.
Or, as an even simpler example, duplicating the rendering of a selected option in some sort of select-input control. In that case, one of the options needs to be rendered twice: once in the list of options and a second time in the representation of the selected option.
To explore this, I wanted to try to create a simple select control in which the user can provide an arbitrary list of select items. Then, when the user goes to select one of the items, the content of the selected item is duplicated into the "selected item" representation of the control.
This demo requires two components: the SelectList and the SelectItem. As much as I could, I tried to keep all of the platform-specific assumptions in one place. So, for example, the SelectItem makes no assumptions about the platform. It only knows about its own host element wrapper (elementRef), which it passes up to the SelectList upon request.
The SelectList, on the other hand, is where all of the platform assumptions are made. It provides and consumes the BrowserDomAdapter and it understand the concept of innerHTML. But, even so, it still tries to interact with the innerHTML through the API of the BrowserDomAdapter instead of going directly to the native browser API. As least, with this approach, the BrowserDomAdapter service can still be mocked out or implemented in other contexts.
As you can see, the SelectList is using the .getInnerHTML() and the .setInnerHTML() methods of the BrowserDomAdapter service rather than referencing the .innerHTML node properties directly. This way, those methods can be mocked out, or implemented, in other contexts. So, while this approach is more platform-specific, at least there is some safety to it.
And, when we run the above code and select one of the SelectItem elements, we can see that the item content is cloned successfully:
From what I have seen, the Angular team has yet to release any "best practice" guides around the platform-agnostic nature of the Angular 2 framework. As such, it feels very much like unchartered territory every time that I need to interact with the DOM in an imperative way instead of a declarative way. So, right now, my strategy for success is to just try and keep those operations - like copying innerHTML - behind some sort of an API that can be mocked or implemented in other contexts.
Want to use code from this post? Check out the license.
According to Rob Wormald "Renderer" is what you want to use if you need to touch the DOM and be cross platform.
Right; but, I don't see any obvious way to inject HTML with the Renderer class. There is a .projectNodes(); but that moves nodes from place to another, it doesn't copy them. There's also a .invokeElementMethod() method, so I thought maybe I could call "clone" on the target node. But, alas, the invokeElementMethod() doesn't actually return its results; so, even if it is cloning nodes, it's not giving me access to them.
If you can figure it out, I would love to use something more generic.
Good point. What I mainly wanted to point out is using this BrowserDomAdapter locks you out of service workers and cross platform AFAIK. Angular2 still has a lot of docs to fill and things to implement so it could be an unfinished feature.
I was digging through this and found an alternative in some test code. This works, both in a SPA and in Angular Universal (just tried it):
There is no documentation as of yet.
Funny enough, I just used [innerHTML] in my dropdown menu demo, where I had to inject the selected option's HTML into the root of the menu:
But, even with the [innerHTML] property, it feels unsafe. Meaning, can we really be sure it accepts a string? Consider that I have a component controller that does something like:
this.content = "hello!";
... and I wanted to render that. Sure, [innerHTML] works in a browser, but what about somewhere else, like server-side rendering or Native Script? Will those views be able to understand [innerHTML] in terms of HTML strings?
The decoupling of the renderer from the core has allowed for large companies like Telerik and Ionic to create some amazing 3rd party libraries for Angular 2. But it is starting to become more apparent that there is a cost to having all the abstraction layers in Angular 2 for the average developer. Trying to do something as "simple" as getting a reference to a native HTML DOM element seems to have a lot of complexity if you want to stay consistent with doing things the Angular 2 way... which as you pointed out, it is not evident on what that method actually is yet.
In just about every answer on StackOverflow on the topic of Angular 2, there is at least one comment along the lines of... "don't do this because it will tightly couple your app to the browser". I'm only now beginning to feel like this mentality may be becoming a bit too extreme. There is this sense that seems to be growing that discourages any complex DOM manipulation. But that is exactly where all the next great web applications are to be had!
Having portable code really would be nice... but until there is an elegant approach, it is only slowing down development time for me. So I say to hell with it. I'm going to use ElementRef all over the damn place. It is the simplest way to get to the DOM, so that's what I'm going to use.
Thanks for the article. It was the affirmation I needed. I'm not the only one unsure of how to create portable code after all. So I'm going to forget it entirely and leverage the framework in the areas that it does do well at. Cheers ^_^