Extending native HTML element in Angular 6 - angular6

I have recently created a native web component which is working well in all browsers. I moved this web component into an Angular 6 application and all works as expected. I then tried to extend a native HTML element which again worked perfectly except when I brought it into my Angular 6 application.
Using the examples from Mozilla I will try and illustrate my issue. Using the following trying to extend a native 'p' element:
// Create a class for the element
class WordCount extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// count words in element's parent element
var wcParent = this.parentNode;
function countWords(node){
var text = node.innerText || node.textContent
return text.split(/\s+/g).length;
}
var count = 'Words: ' + countWords(wcParent);
// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});
// Create text node and add word count to it
var text = document.createElement('span');
text.textContent = count;
// Append it to the shadow root
shadow.appendChild(text);
// Update count when element content changes
setInterval(function() {
var count = 'Words: ' + countWords(wcParent);
text.textContent = count;
}, 200)
}
}
// Define the new element
customElements.define('word-count', WordCount, { extends: 'p' });
<p is="word-count">This is some text</p>
By taking that same code and putting it into an Angular 6 application, the component never runs. I put console log statements in the constructor and connectedCallback methods and they never trigger. If I remove the {extends: 'p'} object and change the extends HTMLParagraphElement and make it an extend HTMLElement to be an autonomous custom element everything works beautifully. Am I doing something wrong or does Angular 6 not support the customized built-in element extension?

I assume the reason is the way that Angular creates those customized built-in elements when parsing component templates - it probably does not know how to properly do that. Odds are it considers is a regular attribute which is fine to add after creation of the element (which it isn't).
First creating the element and then adding the is-attribute will unfortunately not upgrade the element.
See below example: div#d has a non-working example of that customized input.
customElements.define('my-input', class extends HTMLInputElement {
connectedCallback() {
this.value = this.parentNode.id
this.parentNode.classList.add('connected')
}
}, {
extends: 'input'
})
document.addEventListener('DOMContentLoaded', () => {
b.innerHTML = `<input type="text" is="my-input">`
let el = document.createElement('input', {
is: 'my-input'
})
el.type = 'text'
c.appendChild(el)
// will not work:
let el2 = document.createElement('input')
el2.setAttribute('is', 'my-input')
el2.type = 'text'
d.appendChild(el2)
})
div {
border: 3px dotted #999;
padding: 10px;
}
div::before {
content: "#"attr(id)" ";
}
.connected {
background-color: lime;
}
<div id="a"><input type="text" is="my-input"></div>
<div id="b"></div>
<div id="c"></div>
<div id="d"></div>
So to get it to work with Angular, hook into the lifecycle of your Angular component (e.g. onInit() callback) and pick a working way to create your element there.

Related

How and when to unobserve an Intersection Observer

I am using a few Intersection Observers to simply change some classes when certain elements are in the viewport. While it's working, I thought it would be best practice to unobserve these when the elements are not in the viewport, with regard to performance and memory build-up. But, inspecting JS events shows that there are only events fired when the elements are in the viewport, and honestly I'm not sure if that alone is just fine.
Here is the working code I have, but note that if I uncomment observerInfo.unobserve(infoStatement); it no longer works, which is expected, but I am not sure how to implement that unobserve.
const infoStatement = document.querySelector('.statement-container');
const bodyInfo = document.querySelector('body');
const observerInfo = new IntersectionObserver(
function callBackFunction(entries) {
const [entry] = entries;
if (entry.isIntersecting) {
bodyInfo.classList.add('background-dark');
} else {
bodyInfo.classList.remove('background-dark');
//observerInfo.unobserve(infoStatement);
}
},
{ rootMargin: "-10%", }
)
observerInfo.observe(infoStatement);
FIDDLE is here
I also tried a toggle in the if/else statement, but that only added the class and did not remove it:
if (entry.isIntersecting) {
bodyInfo.classList.toggle('background-dark');
} else {
observerInfo.unobserve(infoStatement);
}
How can I achieve this?

Getting pixel value of css style through Angular

I'm trying to get the exact pixel value of a margin-left style applied to a div element with the calc() function, I need this value sent to a function in my typescript, so far my code for this looks as follows
HTML:
<div id="cont" class="container" [style.width.px]="getWidth()"
[ngStyle]="getOffset([style.margin-left])">
TypeScript:
getOffset(marg){
var style;
if(marg > 130){
style = {'margin-left': 'calc(50% -'+String(this.offsets[this.arrPosWidth])+'px)' };
}else{
style = {'margin-left': '130px' };
}
return style
}
Sending the style through that function was a long shot and I've tried accessing the margin-left style with element.style but this just returns a literal string with calc(50%-..px). Is there any way for me to get the value in pixels for the margin-left style applied to that particular element?
You can achieve this through the ngAfterContentInit Lifecycle hook.
Here is a working example: https://stackblitz.com/edit/angular-dbfp8x
export class AppComponent implements AfterViewInit {
// this is what we initially set the margin to
public margin:number = 75;
// Bound to your element
#ViewChild('myElem') element: ElementRef;
ngAfterViewInit(){
// get the current margin and then strip off the 'px'
let currentMargin:number = parseInt((this.element.nativeElement.style.marginLeft as string).substring(0,2));
// add any restriction logic you need.
if(currentMargin > 25){
this.margin = 25;
}
}
}

Get incorrect offsetWidth and offsetHeight values

Here is my angular2 code.
Template
<div #picker class="slider">
<div class="slider-track">
<div #sliderSelectionEl class="slider-selection"></div>
<div #sliderHandle1 class="slider-handle"></div>
<div #sliderHandle2 class="slider-handle"></div>
</div>
<div #tooltipEl class="tooltip">
<div class="tooltip-arrow"></div>
<div #tooltipInner class="tooltip-inner"></div>
</div>
<input type="text" class="span2" value="" id="sl2"><br/>
</div>
Component
import {Component, OnInit, Input, ViewChild, ElementRef, Renderer} from '#angular/core';
export class SliderComponent implements OnInit {
#ViewChild('picker') picker: ElementRef;
constructor(private renderer: Renderer, private el: ElementRef) {
}
ngAfterViewInit() {
this.renderer.setElementClass(this.picker.nativeElement, 'slider-horizontal', true);
console.log(this.picker.nativeElement.offsetWidth);
console.log(this.picker.nativeElement.offsetHeight);
}
}
.slider-horizontal {
width: 210px;
height: 20px;
}
The problem is the printed values are different for each time loading. I guess this issue is due to the browser have not completed loading the div. Do you know what is the solution for this?
You can detect size changes by using
MutationObserver
Probably the biggest audience for this new api are the people that
write JS frameworks, [...] Another use case would be situations where you are using frameworks that manipulate the DOM and need to react to these
modifications efficiently ( and without setTimeout hacks! ).
Here is how you can use it to detect changes in elements :
// select the target node
var target = document.querySelector('#some-id'); // or
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true }
// pass in the target node, as well as the observer options
observer.observe(target, config);
// later, you can stop observing
observer.disconnect();
For your case, you could use it inside your ngAfterViewInit and refresh your offsets size. You can be more specific and only detect some mutations, and only then extract your offsets.
more info :
doc: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
compatibility : https://caniuse.com/#feat=mutationobserver
Demo:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation);
if(mutation.attributeName == 'class') // detect class change
/*
or if(mutation.target.clientWidth == myWidth)
*/
showOffset(mutation.target);
observer.disconnect();
});
});
var config = { attributes: true}
var demoDiv = document.getElementById('demoDiv');
var logs = document.getElementById('logs');
// wait for document state to be complete
if (document.readyState === "complete") {
ngAfterViewInit();
}
document.onreadystatechange = function () {
if (document.readyState === "complete") {
ngAfterViewInit();
}
}
// observe changes that effects demoDiv + add class
function ngAfterViewInit(){
observer.observe(demoDiv, config);
demoDiv.classList.add('slider-horizontal');
}
// show offsetWidth + height.
// N.B offset width and height will be bigger than clientWidth because I added a border. If you remove the border you'll see 220px,20px
function showOffset(element){
offsetMessage = "offsetWidth:" + demoDiv.offsetWidth + " offsetHeight: " + demoDiv.offsetHeight;
console.log(offsetMessage);
logs.innerHTML = offsetMessage;
}
.slider-horizontal {
border: 2px solid red;
width: 210px;
height: 20px;
background: grey;
}
<div id='demoDiv'> I am a demo div </div>
<div style="margin-top: 20px;"> logs : <span id='logs' style=" padding: 5px; border: 1px solid black"></span></div>
You have to schedule calls to 'offsetWidth' after rendering cycle, angular executes draw on the end of microtask queue, so you could try setTimeout(..., 0) or run Promise.resolve().then(...) outside of zonejs. Hope it helps.
In order to get correct offset values you can use: ngAfterContentChecked with the AfterContentChecked Interface.
This method is called after every change detection run. So, inside this method use a flag (or counter) and setTimeOut:
if (this.counter <= 10) {
// this print offsetwidth of my element
console.log('mm ' + this.container.nativeElement.offsetWidth);
// setTimeOut allow to run another changedetection
// so ngAfterContentChecked will run again
setTimeout(() => { }, 0);
//you could use a counter or a flag in order to stop getting the right width
this.counter++;
}
Hope it helps! Feel free to comment

Adding an angularJS directive based on a parameter value

TL;DR: Is there a way to dynamically set a directive based on a parameter value? Something similar to ng-class for setting css elements, but a way to set the directive based on the value in the scope. I would have the value in the scope so I could call:
<div class="data.directiveType"></div>
When
data.directiveType = "my-directive"
the div would become
<div class="my-directive"></div>
and myDirective would be invoked?
Detailed Question:
What I am trying to do is allow the user to add elements to the web application and I wanted the directive for each element to be added based on what the user clicks.
I have the following Directives:
app.directive("mySuperman", function(){
//directive logic
});
app.directive("myBatman", function(){
//directive logic
});
app.directive("myWonderWoman", function(){
//directive logic
});
I have the following controller
app.controller("ParentCtrl", function($scope){
$scope.superHeros = [];
var superman = {directiveType: "my-superman"};
var batman = {directiveType: "my-batman"};
var wonderWoman = {directiveType: "my-wonder-woman"}
$scope.addBatman = function()
{
var batmanInstance = {}
angular.copy(batman, batmanInstance);
$scope.superHeros.push(batmanInstance);
}
$scope.addSuperman = function()
{
var supermanInstance = {}
angular.copy(superman, supermanInstance);
$scope.superHeros.push(supermanInstance);
}
$scope.addWonderWoman = function()
{
var wonderwomanInstance = {}
angular.copy(wonderWoman, wonderwomanInstance);
$scope.superHeros.push(wonderwomanInstance);
}
});
In the index.html I have
<body ng-controller="ParentCtrl>
<a ng-click="addBatman()">Add Batman</a>
<a ng-click="addSuperman()">Add Superman</a>
<a ng-click="addWonderWoman()">Add WonderWoman</a>
<div ng-repeat="hero in superHeros">
<!-- The following doesn't work, but it is the functionality I am trying to achieve -->
<div class={{hero.directiveType}}></div>
<div>
</body>
The other way I thought of doing this was just using ng-include in the ng-repeat and adding the template url to the hero object instead of the directive type, but I was hoping there was a cleaner way that I could make better use of the data binding and not have to call ng-include just to call another directive.
You can create a directive that takes the directive to add as a parameter, adds it to the element and compiles it. Then use it like this:
<div ng-repeat="hero in superHeros">
<div add-directive="hero.directiveType"></div>
</div>
Here is a basic example:
app.directive('addDirective', function($parse, $compile) {
return {
compile: function compile(tElement, tAttrs) {
var directiveGetter = $parse(tAttrs.addDirective);
return function postLink(scope, element) {
element.removeAttr('add-directive');
var directive = directiveGetter(scope);
element.attr(directive, '');
$compile(element)(scope);
};
}
};
});
Demo: http://plnkr.co/edit/N4WMe8IEg3LVxYkdjgAu?p=preview

HTML service error creating elements dynamically

I'm working with html service of google-apps-script and I'm writing some modules to make life easier. The module below its designed to create elements fast and easy with an easy to read notation.
All works fine except the text function. It uses the input function to create a specific input type. In this case, a text one. When I try to use it I get the following error:
Untaming of guest constructed objects unsupported
I can't understand this error. Could anyone explain it to me?
var util = {
create: (function(){
var element = function (elementName,atributes ){
var element = $( document.createElement(elementName) );
return atributes ? element.attr(atributes) : element;
},
div = function(atributes){ return element('div',atributes); },
textArea = function(atributes){ return element('textarea',atributes); },
input = function(atributes){ return element('input',atributes); },
text = function(atributes){
atributes.type = "text";
return input(atributes);
};
return { div:div, input : input, text:text, textArea:textArea}
})()
};