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().
Related
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.
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;
}
I have a boolean column in my grid which is currently displaying 'true' or 'false' but I want it to show a checkbox instead.
How should I do this.
We are using ag-grid 25 with Angular and Adaptable.
You can write your own cell renderer that renders a checkbox instead of a string. Below is an example:
import { Component, OnDestroy } from '#angular/core';
import { ICellRendererAngularComp } from '#ag-grid-community/angular';
#Component({
selector: 'checkbox-renderer',
template: `
<input
type="checkbox"
(click)="checkedHandler($event)"
[checked]="params.value"
/>
`,
})
export class CheckboxRenderer implements ICellRendererAngularComp, OnDestroy {
private params: any;
agInit(params: any): void {
this.params = params;
}
checkedHandler(event) {
let checked = event.target.checked;
let colId = this.params.column.colId;
this.params.node.setDataValue(colId, checked);
}
}
import { CheckboxRenderer } from "./checkbox-renderer.component";
this.frameworkComponents = {
checkboxRenderer: CheckboxRenderer
};
Live Demo
Resource
https://blog.ag-grid.com/binding-boolean-values-to-checkboxes-in-ag-grid
As you say that you are using AdapTable then you can just add the name of the column to the CheckBoxColumns list in PredefinedConfig / UserInterface
See: https://docs.adaptabletools.com/docs/predefined-config/user-interface-config#checkboxcolumns
So something like:
export default {
UserInterface: {
CheckboxColumns: ['myBooleanColumn'],
},
} as PredefinedConfig;
That will dynamically create a CellRenderer very similar to the one which NearHuscarl suggests.
If the column is ReadOnly the checkboxes will be disabled.
Its worth noting that the CheckboxColumnClickedEvent will fire each time a cell in the column is clicked.
I have a component that uses #HostBinding to set a class:
#HostBinding('class.dark-1') true;
Which works fine. However, now I need to create a function in my component to change the class dynamically.
For example, from dark-1 to light-2 when a button in the component is clicked.
I know how to create the function and call it from a button, but how do I change the class in the hostbinding and refresh the UI with the new class?
You can toggle a clicked flag when clicking the button, and set the classes with getters:
#HostBinding("class.dark-1") public get classDark1() {
return !this.clicked;
}
#HostBinding("class.light-2") public get classLight2() {
return this.clicked;
}
private clicked = false;
public onClick() {
this.clicked = true;
}
Sinply give it a property name:
#HostBinding('class.dark-1') isDark = true;
Then you can change it:
this.isDark = false;
Or change entire className:
#HostBinding('class') className = 'dark-1';
this.className = 'light-1';
I have got my themes all working perfectly, but for some reason my mat-menu will only get themed on whatever default is called and not otherwise.
So for its theme to not be broken I have to call
#include angular-material-theme($dark-theme);
right at the top of my styles.scss and then have my custom classes that I set, which my light is loaded by default as shown here:
import {OverlayContainer} from "#angular/cdk/overlay";
#Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit, AfterViewInit {
title:string = "Callum.Tech";
themeClass: string = "light-theme";
overlay;
constructor(
private overlayContainer: OverlayContainer
) {
this.overlay = overlayContainer;
}
setDarkTheme(): void {
this.themeClass = "dark-theme";
}
setLightTheme(): void {
this.themeClass = "light-theme";
}
ngOnInit() {
this.overlay.themeClass = this.themeClass;
}
}
Everything else will re-theme and work fine without calling the start include I mentioned but mat-menu will throw a fit and only use the first theme its fed on the site launching, and doesnt change with the rest of the theme.
Here is what it looks like with the dark theme called at the start of styles.scss and the light theme loaded like normal
And here is the dark theme chosen, but the dark theme not called at the start of styles.scss:
In the breaking changes for 2.0.0-beta.11:
overlay: Now that the Overlay is part of the cdk rather than Angular Material directly, the themeClass property has been removed. To add a class to the overlay for theming, you can do
overlayContainer.getContainerElement().classList.add('my-theme-class');
So, you can change your code as follows:
constructor(
private overlayContainer: OverlayContainer
) {
this.overlay = overlayContainer.getContainerElement();
}
toggleTheme(): void {
if (this.overlay.classList.contains("dark-theme") {
this.overlay.classList.remove("dark-theme");
this.overlay.classList.add("light-theme");
} else if (this.overlay.classList.contains("light-theme") {
this.overlay.classList.remove("light-theme");
this.overlay.classList.add("dark-theme");
} else {
this.overlay.classList.add("light-theme");
}
}
ngOnInit() {
this.toggleTheme();
}