In this article, we will look at how to create pop-up and overlapping elements on React, Angular 1.5 and Angular 2. We implement the creation and display of a modal window on each of the frameworks. All code is written in typescript. The source code for the samples is available on github .
What are "pop-up and overlapping" elements? These are DOM elements that appear above the main content of the document.
These are various pop-up windows (including modal), drop-down lists and menus, panels for date selection, and so on.
As a rule, absolute positioning is used for such elements in window coordinates (for modal windows) using position: fixed
, or absolute positioning in document coordinates — for menus, drop-down lists that should be located near their “parent” elements — using position: absolute
.
Simply placing pop-up items near the "parents" and hiding / displaying them does not work completely. The reason is parent containers with overflow
other than visible
(and fixed
). Anything that protrudes beyond the container will be clipped. Also, such elements can overlap with elements below the tree, and z-index does not always help here, as it works only within the same context overlay .
In an amicable way, this problem could be elegantly solved by the Shadow DOM (and even then, not a fact), but so far it is not ready. Could help CSS property prohibiting trimming and overlapping, or positioning relative to the document (and not the parent), but it is not. Therefore, we use a crutch - DOM elements for pop-up components are placed in the body
, well, or, at worst, closer to it, in a special container, whose parents obviously do not have “trimming” styles.
Note: we will not consider the positioning of elements with different coordinates and parents - this is a separate complex topic, full of crutches, javascript and handling unexpected browser events.
For example, none of the existing methods ideally solves the positioning problem if we need to make a component of type select
or autocomplete with a drop-down list near the element.
Using position: fixed
, apparently, allows you to avoid clipping the parent container, but it forces you to scroll the document and container (the easiest way to close the drop-down list is stupid). Using position: absolute
and placing the element in the body
handles the scrolling of the document correctly, but requires recalculation of the position when scrolling the container.
All methods require handling the resize event. In general, there is no good solution here.
All examples contain the same layout, and consist of an input field with text and a button. By clicking on the button, the entered text appears in the "modal" window with the "Close" button.
All examples are written in typescript. For compilation and bandling webpack is used. To run the examples, you must have NodeJS installed.
To start, go to the folder with the corresponding example and run npm run prepare
NodeJS command line once to install the global and local packages. Then run the npm run server
. After that, open the browser address http: // localhost: 8080
If you are too lazy to do this, you can simply open in the browser index.html
from the folder of the corresponding example.
In version 1.5, Angular purchased syntactic sugar as a component
method from a module that allows you to declare components. Components are actually directives, but their ad code is focused on creating application domain building blocks, while directives are more oriented (ideologically, technically everything is identical) to low-level and imperative work with the DOM. This innovation is simple, but cool, and allows you to declare components in a way similar to Angular 2. This method does not introduce any new features, but can drastically affect the structure of the application, especially if you used <div ng-controller="MyController as $c">...</div>
.
From myself I can add that I am delighted with this opportunity. It allows you to create components with a clear contract and high potential for reuse. Moreover, with this feature, I refused to render the HTML markup of the component into a separate file, since the markup is small and neat — it uses embedded components — and does not clutter the source of the component.
In the example, I also use this feature, so if you are not familiar with it, you can read it here .
There are probably more ways to put a component in an arbitrary place in the DOM. I will show two of them, one with the help of the $compile
service, the second with the directive with transclude
.
The first method is imperative, and is more suitable for ad-hoc display of modal windows, for example, for displaying messages or prompting the user for some parameters. This method can also be used if the component type is unknown, or the markup is dynamic.
The second method is declarative, it allows you to embed a pop-up element in the component template,
but when showing it, put it in the body
. Suitable for drop-down components, allowing for responsive control of visibility.
The $compile
allows you to convert a string with Angular markup to a DOM element and associate it with $scope
.
The resulting element can be added to an arbitrary place in the document.
It's pretty simple.
Here is the service documentation . The link is a complete guide to the API directives, the part of interest at the very end is the use of $compile
as a function.
We get access to $compile
// popup.service.ts import * as angular from "angular"; export class PopupService { static $inject = ["$compile"]; constructor(private $compile: angular.ICompileService) {} }
The static $inject=["$compile"]
equivalent to the following Javascript code:
function PopupService($compile) { this.$compile = $compile; } PopupService.$inject = ["$compile"];
$compile
works in two phases. At first, it converts a string to a factory function. On the second, you need to call the received factory and pass it $scope
. The factory will return DOM elements associated with this $ scope.
$compile
takes three arguments, we are only interested in the first one. The first argument is a string containing the HTML template, which will then be converted into a working fragment of the Angular application. In the template, you can use any registered components from your module and its dependencies, as well as any valid Angular constructions - directives, string interpolation, etc.
The result of the compilation will be a factory - a function that allows you to associate a string pattern with any $scope
. Thus, specifying a template, you can use any fields and methods of your scopa. For example, this is how the popup window opening code looks like:
/// test-popup.component.ts export class TestPopupComponentController { static $inject = ["$scope", PopupService.Name]; text: string = "Open popup with this text"; constructor( private $scope: angular.IScope, private popupService: PopupService) { } openPopup() { const template = `<popup-content text="$c.text" ...></popup-content> ` this.popupService.open(template)(this.$scope); } }
Pay attention to a few things.
First, the template contains the <popup-content></popup-content>
component.
Secondly, the template contains a call to the controller's text
field: text="$c.text"
.$c
is the controller alias specified when the component is declared.
PopupService.open
also returns a factory that allows you to associate a template with $scope
. In order to associate a dynamic component with the $scope
our component, you have to pass $scope
to the controller.
Here’s what PopupService.open
looks PopupService.open
:
// popup.service.ts open(popupContentTemplate: string): ($scope: angular.IScope) => () => void { const content = ` <div class="popup-overlay"> ${popupContentTemplate} </div> `; return ($scope: angular.IScope) => { const element = this.$compile(content)($scope); const popupElement = document.body.appendChild(element[0]); return () => { body.removeChild(popupElement); }; }; }
In our function, we wrap the passed template in the markup of a modal window. Then we compile the template, getting the factory of dynamic components. Then we call the resulting factory, passing $scope
, and we get the HTML element, which is a fully working fragment of the Angular application associated with the transferred $scope
. Now you can add it anywhere in the document.
Although our PopupService.open
method also returns a factory to communicate with $scope
, it does additional work. First, when the factory is called, it not only creates the element, but also adds it to the body
. Secondly, it creates a function that allows you to "close" the pop-up window, removing it from the document. PopupService.open
returns this function to close the window.
Well, the option is not so bad. Although the window mapping itself is imperative, however, the contents of the window are still reactive, and can be declaratively related to the parent $scope
. Although you have to use strings to display content, but if you make the content of the window itself as a component, you only need to bind the input and output properties, and not all the content. The method allows you to place the pop-up element anywhere in the document, even if it is outside the ng-app
.
The second method allows you to set the contents of the pop-up element right next to its "parent". When displayed, the element will actually be added to the body
.
<div> <div class="form-group"> <label> Enter text to display in popup: </label> <input class="form-control" ng-model="$c.text" type="text" /> </div> <p> <button class="btn btn-default" ng-click="$c.openInlinePopup()"> Open inline </button> </p> <!-- body --> <popup ng-if="$c.popupVisible"> <popup-content text="$c.text" close="$c.closeInlinePopup()"> </popup-content> </popup> </div>
Here the required directive is <popup>...</popup>
. Everything inside it will be shown in a pop-up window, and located in the body
.
A small drawback of this method is that showing and hiding the window is necessary with the help of the ng-if
directive, which will physically remove / add content to the DOM tree.
transclude
is a way of directives to work with its content. By content is meant what is located between the opening and closing tags of the directive.
<my-directive> <!-- , my-directive, --> <div class=""> <other-component>...</other-component> </div> </my-directive>
This is a very powerful feature, on the basis of which you can do a lot of interesting things. We will take the content and put it in the body
.
How to use transclude
? Directly using content (for example, through $element.children
) is impossible - it is not associated with the correct scope, and is not compiled (no directives are replaced, etc.). To use transclude
you need to access the so-called. transclude function
. This is a factory that can create compiled copies (clones) of content. These clones will be compiled and linked to the correct scope, and in general, very similar to the output of $compile
. The transclude function, however, does not return a value like a normal factory, but passes it to a callback function.
You can create as many clones as you like, redefine the scope with it, add the document to any place, and so on. Great.
For directives that control the content themselves (they call the transclude function), you must implement lifecycle methods to clear the contents. These methods are implemented in the directive controller. Delete the added content needed in $onDestroy
.
The last thing left is how to access the transclude function. It is transmitted in several places, but we will inject it into the controller. In order for it to be passed, transclude: true
must be set in the directive configuration.
So, the complete code:
import * as angular from "angular"; export class PopupDirectiveController { private content: Node; constructor(private transclude: angular.ITranscludeFunction) { } $onInit() { this.transclude(clone => { const popup = document.createElement("div"); popup.className = "popup-overlay"; for(let i = 0; i < clone.length; i++) { popup.appendChild(clone[i]); } this.content = document.body.appendChild(popup); }); } $onDestroy() { if (this.content) { document.body.removeChild(this.content) this.content = null; } } } export const name = "popup"; export const configuration: angular.IDirective = { controller: ["$transclude", PopupDirectiveController], replace: true, restrict: "E", transclude: true };
Not bad, just 36 lines.
Benefits:
Disadvantages:
ng-if
to control the display.The new version of Angular, which differs from the first so much that, in fact, it is a new product.
My impressions of him are twofold.
On the one hand, the component code is clearly cleaner and clearer. When writing business components, the separation of code and presentation is excellent, change tracking works fine, goodbye $watch
and $apply
, excellent tools for describing a component contract.
On the other hand, does not leave a feeling of monstrosity. 5 min quickstart looks like a mockery. Many additional libraries, many of which are mandatory (like rxjs
). The fact that I can see Loading ... when opening a document from the file system raises doubts about its speed. The size of the bundle is 4.3MB versus 1.3MB for Angular 1 and 700KB React (but this is no optimizations, default webpack bundling). (I remind you that webpack collects (bandit) all the code of the application and its dependencies (from npm) into one big javascript file).
Minified size: Angular 1 - 156KB, Angular 2 - about 630KB, depending on the version, React - 150KB.
Angular 2 at the time of writing yet RC. The code is almost ready, there seems to be no bugs, the basic things are done (well, except maybe reworking the forms). However, the documentation is incomplete, many things have to be found in the comments to the github issue
(such as dynamic loading of components, which, in fact, prompted me to write this article).
I didn’t want to spend an hour and a half on the steps described in the mentioned 5 min quickstart, so the project is not completely configured, ahem, traditionally for Angular 2. SystemJS is not used, instead it becomes a webpack. Moreover, Angular 2 is not specified as externals, but is taken from the npm package as is. The result is a giant bundle of 4.5MB in weight. Therefore, do not use this configuration in production unless, of course, you want users to hate your download indicator. The second oddity, which does not know what caused it, is the different names of the modules. In all examples (including the official documentation), the Angular import looks like import { } from "@angular/core"
. At the same time, it did not work for me, but import {} from "angular2/core"
works.
To its credit, Angular 2, the dynamic loading code is difficult only when searching. For dynamic loading, the ComponentResolver class is used in conjunction with the ViewContainerRef .
// . // - ( -) loadComponentDynamically(componentType: Type, container: ViewContainerRef) { this.componentResolve .resolveComponent(componentType) .then(factory => container.createComponent(factory)) .then(componentRef => { // componentRef.instance; // ElementRef , componentRef.location; // DOM . componentRef.location.nativeElement; // componentRef.destroy(); }); }
ComponentResolver
easy to get through dependency injection. ViewContainerRef
, apparently, cannot be created for an arbitrary DOM element, and can only be obtained for an existing Angular component. This means that it is impossible to place a dynamically created element in an arbitrary place in the DOM tree, at least in a release candidate.
Therefore, our mechanism for showing pop-ups will be integral.
First, we will have a component to which pop-up elements will be dynamically added. It will need to be placed somewhere in the component tree, preferably closer to the root element. In addition, none of its parent containers should contain styles that trim the contents. In the code, this is overlay-host.component.ts
.
Secondly, we have an auxiliary component that contains markup for pop-up windows. This is the OverlayComponent
in which the dynamically generated component is wrapped.
Thirdly, we have a service that provides communication between the host component for pop-ups and clients who want to show the component. The service is fairly simple, the host component registers itself in it when it is created, and the service method simply redirects calls to open the window to that host component.
I will bring the whole class, it is not very big, and then I will go through thin places:
import { Component, ComponentRef, ComponentResolver, OnInit, Type, ViewChild, ViewContainerRef } from "angular2/core"; import { OverlayComponent } from "./overlay.component"; import { IOverlayHost, OverlayService } from "./overlay.service"; @Component({ selector: "overlay-host", template: "<template #container></template>" }) export class OverlayHostComponent implements IOverlayHost, OnInit { @ViewChild("container", { read: ViewContainerRef }) container: ViewContainerRef; constructor( private overlayService: OverlayService, private componentResolver: ComponentResolver) { } openComponentInPopup<T>(componentType: Type): Promise<ComponentRef> { return this.componentResolver .resolveComponent(OverlayComponent) .then(factory => this.container.createComponent(factory)) .then((overlayRef: ComponentRef) => { return overlayRef.instance .addComponent(componentType) .then(result => { result.onDestroy(() => { overlayRef.destroy(); }); return result; }); }); } ngOnInit(): void { this.overlayService.registerHost(this); } }
What does this code do? It dynamically creates a component using its type (the type of the component is its constructor function). A pre-created component wrapper ( OverlayComponent
), our requested component is added to it already. We also subscribe to the destroy
event to destroy the wrapper when the component is destroyed.
The first thing you need to pay attention to is how the ViewContainerRef
is ViewContainerRef
by querying the content.
The @ViewChild()
decorator allows you to get a ViewContainerRef
called template variable template: <template #container></template>
.#container
is the template variable, the template variable. It can be accessed by name, but only in the template itself. To access it from a component class, the decorator is used.
Honestly, I googled it, and as for me, it is generally unintuitive. This is one of the features of the second Angular, which struck me very much - it’s very difficult, or impossible, to find solutions for typical low-level directive development tasks in the documentation. Documentation for the creation of business components is normal, and there is nothing particularly complicated there. However, it is impossible to find documentation for scripts for writing controls, low-level components. Dynamic creation of components, interaction with a template from a class — these areas are simply not documented. Even in the @ViewChild description , nothing is said about the second parameter.
Well, I hope, to release will document.
The OverlayHostComponent
code, which I cited above, is the most interesting in our example. OverlayComponent
contains similar code for dynamically adding content, OverlayService
redirects calls to open pop-up to the host component. I do not list listings because of the triviality, if you're interested, look at the source.
Now let's see how to use it:
import { Component, Input } from "angular2/core"; import { OverlayService } from "./overlay"; import { PopupContent } from "./popup-content.component"; @Component({ selector: "test-popup", template: ` <div> <div class="form-group"> <label> Enter text to display in popup: </label> <input class="form-control" [(ngModel)]="text" type="text" /> </div> <p> <button class="btn btn-primary" (click)="openPopup()"> Open popup </button> </p> </div> ` }) export class TestPopup { text: string = "Show me in popup"; constructor(private overlayService: OverlayService) { } openPopup() { this.overlayService .openComponentInPopup(PopupContent) .then(c => { const popup: PopupContent = c.instance; popup.text = this.text; popup.close.subscribe(n => { c.destroy(); }); }); } }
OverlayService
providers Root
, .
ComponentRef.instance
.
: , , . . , . isRunning
.
, . , , - , DOM , .
ngFor , . , , directives
.
DOM , , -. , , .
DOM — render
, DOM. , , . render
null
, lifecycle- componentDidMount
, componentWillUnmount
, componentDidUpdate
. componentDidMount
componentDidUpdate
, ReactDOM.render
, . componentWillUnmount
, , .
, :
import * as React from "react"; import * as ReactDOM from "react-dom"; export class Popup extends React.Component<{}, {}> { popup: HTMLElement; constructor() { super(); } render() { return (<noscript></noscript>); } componentDidMount() { this.renderPopup(); } componentDidUpdate() { this.renderPopup(); } componentWillUnmount() { ReactDOM.unmountComponentAtNode(this.popup); document.body.removeChild(this.popup); } renderPopup() { if (!this.popup) { this.popup = document.createElement("div"); document.body.appendChild(this.popup); } ReactDOM.render( <div className="popup-overlay"> <div className="popup-content"> { this.props.children } </div> </div>, this.popup); } }
Everything is simple and clear. , .
Angular . , , . DOM , . - children
, HTMLElement, , - ( React.Children
).
, , . , , render
:
render() { return ( <div> <div className="form-group"> <label> Enter text to display in popup: </label> <input className="form-control" value={ this.state.text } onChange={ e => this.setText(e.target.value) } type="text" /> </div> <p> <button className="btn btn-primary" onClick={e => this.openPopup() } type="button" > Open popup </button> </p> <Ifc condition={ () => this.state.isPopupVisible } > <Popup> <div className="alert alert-success"> <h2> { this.state.text} </h2> <button className="btn btn-warning" onClick={e => this.closePopup() } type="button" > Close popup </button> </div> </Popup> </Ifc> </div> ) }
Ifc
, , condition
. IIFE, .
: <Popup></Popup>
— - , — . DOM body
.
, Angular 1.5, .
, - , Angular $compile
. . ( ReactDOM.render
), , openPopup
. , , , , .
— .
, — , , input/output — Angular: by design Angular 2, Angular 1.5. Angular.
— , - CSS, -. , " " " " . , Angular 2 , , . , Angular React ( ), .
Source: https://habr.com/ru/post/305892/
All Articles