How to get the object created by customElement in a Web Component? - web-component

I am building a Web Component to be used in a Framework, which embeds a grid libary.
I have managed to get the grid to display by wrapping it in an HTMLElement Class
export default class DataGrid extends HTMLElement {
constructor() {
super();
let tmpl = document.createElement('template');
tmpl.innerHTML = `
<div id="lib-datagrid"></div>
`;
this._shadowRoot = this.attachShadow({mode: "open"});
this._shadowRoot.appendChild(tmpl.content.cloneNode(true));
this._rowData = [];
...
}
and
// Load the grid
customElements.define('my-grid', DataGrid);
I need to be able to pass data into the Grid via a DataGrid instance. However it seems that createElements.define() takes a Class rather than an object instance, so I don't have the option to create an Instance (new DataGrid()) and pass that in.
My theory is that I should be able to retrieve the created element via the dom tree, but my Web Component lives within a Shadow Dom and my Web Component isn't itself a Dom element (I think) i.e. no "this.getRootNode()" etc, but do have access to document and window.
Am I missing something in the createElement process or is there a way to find the root node of the current shadow dom?
** Edit - adding Top level WebComponent view
export default (state) => {
const { items, alert, loading } = state;
return (
<div>
<div className="card-top">
<my-grid></my-grid>
</div>
</div>
);
};
** Edit 2
I have found that coding the class extends HTMLElement in-line (rather than in a seperate js file) does allow me to update a reference from the objects connectedCallback() function.
let myobject = null;
customElements.define('my-grid2', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<p>
Hello
</p>`;
myobject = this;
}
});
Pending other suggestions - I will work with this and post an answer if it works out.

Note that as soon as you have access to an instance of your custom element, you can simply use constructor theft to create new instances:
// let's say you have a reference to `<my-grid />` in `el`:
const newEl = new el.constructor;

After customElements.define('my-grid', DataGrid), you have to actually create an element using:
const elm = document.createElement('my-grid');
const orElm = new DataGrid();
// Use `elm` or `orElm` to pass the data.
If your web component is added to DOM tree by some other means, then you must locate it using querySelector or equivalent like:
const elm = document.querySelector('my-grid');
But if it is nested within some other shadow-root, then querySelector won't be able to do it. For this purpose, you would have to precisely find the parent element, get its shadow root and then run querySelector on that shadow root, something like:
const shadowroot = parentElement.shadowRoot;
shadowroot.querySelector('my-grid');
On a side note, if you need to query or send the data from outside your web component, then it is probably code smell as you are breaking laws of encapsulation. There are other better ways to pass the data to the child component. Or else, you don't need shadow DOM API. Just use custom elements without shadow DOM.

I have found a way to do this.
By defining the HTMLElement class in-line and handling the grid wrapper object in the connectedCallback() function, I can get access to references for both the element created and the DataGrid wrapper object.
All the DataGrid wrapper requires is the shadowRoot created as a constructor parameter.
let myElement = null;
let myDataGrid = null;
// Load the grid
customElements.define('my-grid', class extends HTMLElement {
connectedCallback() {
let tmpl = document.createElement('template');
tmpl.innerHTML = `
<div id="lib-datagrid"></div>
`;
const shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(tmpl.content.cloneNode(true));
myDataGrid = new DataGrid(shadow);
myElement = this;
}
});
With that I can now, later on, call a function on the DataGrid object to update data.

Just to save you typing
You can write this:
connectedCallback() {
let tmpl = document.createElement('template');
tmpl.innerHTML = `
<div id="lib-datagrid"></div>
`;
const shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(tmpl.content.cloneNode(true));
myDataGrid = new DataGrid(shadow);
myElement = this;
}
as:
connectedCallback() {
this.attachShadow({mode: 'open'}).innerHTML = `<div></div>`;
myDataGrid = new DataGrid(this.shadowRoot);
myElement = this;
}

Related

Calling one web component from another

I have created two web components that live in separate files. When I try to reference one in the other using an import I get an error
Uncaught SyntaxError: Unexpected token '-'
My code
File1
class element1 extends HTMLElement {
constructor() {
super();
const li = document.createElement('li');
this.shadowRoot.append(li);
}
}
customElements.define('my-element1', element1);
File 2
import {my-element1} from ".element1.js"
class element2 extends HTMLElement {
constructor() {
super();
const ol = document.createElement('ol');
ol.appendChild(document.createElement('my-element1'));
this.shadowRoot.append(ol);
}
}
customElements.define('my-element2', element2);
Why you want to use web components and coupling the components using imports?. If you use web components is to have independants components. If you add an import, the two components there will be always coupled.
You can check this answer which is clearer.
The best way to comunicate between elements is using events. Check this other
By the way, here you don't need events, because your components are defined, so you only need to create a document with the component name like this:
class element1 extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.append("Hello from Component1");
}
}
customElements.define('my-element1', element1);
class element2 extends HTMLElement {
constructor() {
super();
const element1 = document.createElement('my-element1');
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.append("Hello from Component2, let me add Component1...")
shadowRoot.append(document.createElement('br'))
shadowRoot.append(element1);
}
}
customElements.define('my-element2', element2);
<html>
<body>
<my-element2></my-element2>
</body>
</html>
Note how in the DOM only my-element2 is called. And into the component, the number 1 is added.
Then you can achieve your goal playing with this, creating component1 from component2.
my-element1 isn't a valid JavaScript identifier and File1 isn't exporting anything. If you want to import specific symbols you have to first export them.
File 1
export class Element1 extends HTMLElement {
}
File 2
import {Element1} from "./element1.js"
The only reason you would import an component's JavaScript class though is if you need a handle to it. For example you could import the class and define the custom element in the second file.
File 1
export class Element1 extends HTMLElement {
}
File 2
import {Element1} from "./element1.js"
customElements.define('my-element1', Element1);
This is not a recommended pattern though as it gets complicated. If you want to add a third file that also depends on element1, my-element1 can only be defined once.
You can keep the code you currently have and make it work by importing file 1 without any symbols. customElements.define makes defined custom elements globally available so you have to make sure the component is loaded when you plan on using it but you don't need to import the class explicitly.
File 1
class Element1 extends HTMLElement {
}
customElements.define('my-element1', Element1);
File 2
import "./element1.js"
class Element2 extends HTMLElement {
}
customElements.define('my-element2', Element2);
Note that relative file imports in the same directory typically start with ./ and JavaScript class names typical start with capital letters.

SVG.js not drawing svg into container (Polymer)

I've installed svg.js using NPM and imported the module into my custom element file via import "svg.js";,
import "svg.js";
class MyView2 extends PolymerElement {
static get template() {
return html`
<div id="drawing"></div>
`;
}
connectedCallback() {
super.ready();
var draw = SVG("drawing").size(300, 300);
var rect = draw.rect(100, 100).attr({ fill: "#f06" });
}
}
This should insert an SVG inside of the div, but is it's throwing this error in the console instead:
svg.js:3060 Uncaught TypeError: Cannot read property 'nodeName' of null
at new create (svg.js:3060)
at globalRef.SVG (svg.js:33)
at HTMLElement.connectedCallback (my-view2.js:42)
at my-view2.js:50
I'm fairly new to Polymer so not sure if i'm missing something obvious.
Your component has a Shadow Dom so SVG can't find it. Please try this:
let node = this.shadowRoot.querySelector('drawing');
let draw = SVG.adopt(node);
let rect = draw.rect(100, 100).attr({ fill: "#f06" });

How to add style class to a created ComponentRef in Angular 5?

I am trying to add a CSS class to a component immediately after I create it using ViewContainerRef and ComponentFactoryResolver. I want to be able to set the class based on what other Components have already been added to myViewContainerRef.
export class ContainerComponent {
#ViewChild('myContainerRef') myContainerRef: ViewContainerRef
constructor(private factoryResolver: ComponentFactoryResolver,
private renderer: Renderer2) {}
addComponent() {
const componentFactory = this.factoryResolver.resolveComponentFactory(SomeBaseComponent)
const newComponent = this.myContainerRef.createComponent(componentFactory)
// SomeBaseComponent has been added successfully to myContainerRef
// Want to add CSS class to the newComponent
// None of the following statements are adding any styles
if( someLogic()) {
this.renderer.addClass(newComponent.location.nativeElement, 'my-css-class')
this.renderer.addClass(newComponent.el.nativeElement, 'my-css-class')
this.renderer.setStyle(newComponent.el.nativeElement, 'background', 'yellow')
}
}
}
export class SomeBaseComponent {
constructor(public el: ElementRef) {}
}
Is there a better way to go about trying to add the style programmatically? Is there something else I can inject into SomeBaseComponent to be able to add the styles I want at this point, or should I set flags on the newComponent.instance and have the base component be in control of what styles to set on itself?
You should add another #ViewChild which will have a read of ElementRef type.
#ViewChild("myContainerRef", {read: ElementRef}) elementRef: ElementRef;
To set the class attribute, you should use the following.
this.elementRef.nativeElement.setAttribute("class", "test")
Note: I will advised putting the creation logic inside an ngOnInit() method, inside of a custom addComponent().

Changing the scrolling amount of a spark DataGrid when using the mouse wheel

Basically what the title says. I checked DataGridSkin, and it has a Scroller skin part, but I can't find any attribute for the scrolling amount. I might have missed it.
Can anybody please help me on this issue? Thanks.
I suggest the following. You can find part of scrolling logic in VScrollBar class in mouseWheelHandler method.
var nSteps:uint = Math.abs(delta);
...
for (var vStep:int = 0; vStep < nSteps; vStep++)
{
var vspDelta:Number = vp.getVerticalScrollPositionDelta(navigationUnit);
...
In my case delta equals 3. And grid is scrolled for 3 renderers heights. You can extend VScrollBar class. Then create custom ScrollerSkin (copy from original one) and set you vertical bar there instead of default one. Then create custom DataGrid skin (again copy from original one) and set your custom scroller skin for Scroller there.
In custom VScrollBar class override mouseWheelHandler method. Example:
package {
import flash.events.Event;
import flash.events.MouseEvent;
import mx.core.IInvalidating;
import mx.core.mx_internal;
import mx.events.FlexMouseEvent;
import spark.components.VScrollBar;
import spark.core.IViewport;
import spark.core.NavigationUnit;
import spark.utils.MouseEventUtil;
use namespace mx_internal;
public class MyVScrollBar extends VScrollBar {
public function MyVScrollBar() {
}
override mx_internal function mouseWheelHandler(event:MouseEvent):void {
const vp:IViewport = viewport;
if (event.isDefaultPrevented() || !vp || !vp.visible || !visible)
return;
var changingEvent:FlexMouseEvent = MouseEventUtil.createMouseWheelChangingEvent(event);
if (!dispatchEvent(changingEvent))
{
event.preventDefault();
return;
}
const delta:int = changingEvent.delta;
var nSteps:uint = 1; //number of renderers scrolled!
var navigationUnit:uint;
var scrollPositionChanged:Boolean;
navigationUnit = (delta < 0) ? NavigationUnit.DOWN : NavigationUnit.UP;
for (var vStep:int = 0; vStep < nSteps; vStep++)
{
var vspDelta:Number = vp.getVerticalScrollPositionDelta(navigationUnit);
if (!isNaN(vspDelta))
{
vp.verticalScrollPosition += vspDelta;
scrollPositionChanged = true;
if (vp is IInvalidating)
IInvalidating(vp).validateNow();
}
}
if (scrollPositionChanged)
dispatchEvent(new Event(Event.CHANGE));
event.preventDefault();
}
}
}
Set nSteps variable to number of renderers you want to be scrolled. So in this example grid is scrolled by only one item on wheel.
I found a solution: I used the idea described above, but instead of creating a class that extends VScrollBar, I added a MouseWheelChanging event listener to the scroll bar in the Scroller skin:
<fx:Component id="verticalScrollBarFactory">
<s:VScrollBar visible="false" mouseWheelChanging="outerDocument.vScrollBarWheelChanging(event)"/>
</fx:Component>
internal function vScrollBarWheelChanging(event:MouseEvent):void
{ event.delta/=Math.abs(event.delta); }
You can set event.delta to the desired scroll amount.

TextField in AS3 - programming a click listener

I want to add a simple piece of text to the stage and add a listener to do something when the user clicks it.
Here's my TextLink class:
package some.package
{
import flash.display.Sprite;
import flash.external.ExternalInterface;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
public class TextLink extends Sprite
{
public var tf:TextField = new TextField();
public var bspr:Sprite = new Sprite();
public function TextLink(tx:int, ty:int, tft:String):void
{
tf.text = tft;
tf.x = tx;
tf.y = ty;
tf.autoSize = TextFieldAutoSize.LEFT;
bspr.addChild(tf);
this.addChild(tf);
}
}
}
And here is the way I am calling it, along with the listener:
public function test_array_of_objects():void
{
var tmp:TextLink = new TextLink(30, 30, "some text");
tmp.addEventListener(MouseEvent.CLICK, roverNotify);
addChild(tmp);
}
protected function roverNotify(e:Event):void
{
ExternalInterface.call("console.log", "got a click");
}
...But I don't get a message for some reason.
I've imported everything successfully. Any thoughts on what else I can try?
Is your TextLink class an event dispatcher? You're trying to add a listener to the TextLink object, but the click listener needs to be attached to the text field that you're using inside TextLink. TextLink needs to be a DisplayObject of some kind to inherit the dispatching capabilities.
Also, constructors should not specify a return type (since they're just returning themselves) -- the :void should not be there where your TextLink constructor is.
Does function TextLink require something like this at the beginning:
var tf:Text = new Text();
Is the problem with clicking the Sprite or getting the event to fire? If it's the former you could try adding the code below.
tmp.mouseChildren = false;
tmp.buttonMode = true;
ExternalInterface.call("console.log", "got a click");
You have a JavaScript function defined like this??:
function console.log(inputString) {
//do something
}
Edit: Nevermind the above, forgot about Firebug.
Also, TextLink doesn't need to be an event dispatcher, though you may want to have TextLink set its mouseChildren property to false (unless you need to be able to select that text), so that you don't inadvertently trigger events on the TextField, and buttonMode to true.
Edit: Also, what's the point of?:
var bspr:Sprite = new Sprite();
bspr.addChild(tf);
Final edit
How about this? http://code.google.com/p/fbug/issues/detail?id=1494
Yes, you are correct, in FF3 the console is injected only when the page has
javascript and uses window.console.
If you put any js that accesses the console before the Flash loads it should work, eg
<script>
var triggerFirebugConsole = window.console;
</script>
Let us know if this works. It's unlikely that we can fix this soon.

Resources