Is it possible to make React-like compound components in lit 2.0? - web-component

I tried to build compound components with Lit 2.0 but passing data to slots as attributes seems impossible.
<my-accordion>
<my-accordion-title>Title</my-accordion-title>
<my-accordion-content>Content</my-accordion-content>
</my-accordion
How I can pass "extended" propery to custom elements slots?
Here is my custom elements:
#customElement("my-accordion")
export class MyAccordion extends LitElement {
#property()
extended: boolean = false;
toggleExtend(){
this.extended = !this.extended
}
render() {
return html`
<div #click=${this.toggleExtend}>
<slot .extended=${this.extended}></slot>
</div>
`;
}
}
#customElement("my-accordion-title")
export class MyAccordionTitle extends LitElement {
// want to access parent node extended property here
render() {
return html`
<div>
<slot></slot>
</div>
`;
}
}

To assign the extended property to slotted children, get the slot element's assignedElements() array.
#customElement("my-accordion")
export class MyAccordion extends LitElement {
#property()
extended: boolean = false;
toggleExtend(){
this.extended = !this.extended
}
updated(changed: PropertyValues<this>) {
if (changed.has('extended'))
this.extendedChanged()
}
extendedChanged() {
for (const child of this.slot.assignedElements()) {
if (child instanceof MyAccordionTitle)
child.extended = this.extended;
}
}
#query('slot') slot: HTMLSlotElement | null;
render() {
return html`
<div #click=${this.toggleExtend}>
<slot></slot>
</div>
`;
}
}
#customElement("my-accordion-title")
export class MyAccordionTitle extends LitElement {
// want to access parent node extended property here
render() {
return html`
<div>
<slot></slot>
</div>
`;
}
}
NB: When assigning click listeners to <div> and other non-interactive elements, there are many accessibility issues involved. It's usually recommended therefore to use a <button> element, or in your case maybe a <details> and <summary>

Related

Using Hostlistner and directive to add CSS class

I have created a directive and using HostListner , want to add a CSS style on Click event on the tag.Also remove on the click again.I have the following CSS .
CSS
.strikethrough { text-decoration: line-through;}
HTML
<p optionalIngredient>ABCD</p>
Directive
constructor(private elRef: ElementRef ,private renderer: Renderer2)
{ }
#HostListener('mouseenter') onMouseEnter() {
this.renderer.addClass(this.elRef.nativeElement, 'strikethrough');
}
#HostListener('mouseleave') onMouseLeave() {
this.renderer.removeClass(this.elRef.nativeElement,strikethrough');
}
You can use a boolean to keep track of whether the style is applied.
styled = false;
#HostListener('click')
onClick(){
if (!styled) this.renderer.addClass(this.elRef.nativeElement, 'strikethrough');
else this.renderer.removeClass(this.elRef.nativeElement, 'strikethrough');
this.styled = !this.styled;
}

How do I make my boolean column in ag-grid show checkboxes

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.

How to make PrimeNG Dropdown Keyboard Accessible

I want to make dropdown keyboard accessible. Right now, its not working when i am using keyboard up and down arrow. I applied tabindex but still not working. Anyone have any idea about this..
<p-dropdown [options]="cities" tabindex="1" placeholder="Select an option"></p-dropdown
I tried this it working properly using keyboard up and down arrow :
npm version :
"primeng": "^4.1.3"
in html file
<p-dropdown [options]="cities" [(ngModel)]="selectedCity" tabindex="1" placeholder="Select an option"></p-dropdown>
module.ts
import { DropdownModule } from 'primeng/primeng';
#NgModule({
imports: [
DropdownModule
]
})
component.ts
import {SelectItem} from 'primeng/primeng';
cities: SelectItem[];
selectedCity: string;
PrimeNG not support reading dropdown options by default. Default behavior is navigation by arrows and change value. Default tabulator key action is hide overlay with element items. My solution forces tabulator key to choose dropdown option and when you hit enter key the value is changed.
I prepared angular directive which override default dropdown behavior. You can add add this directive do module and import it in place where you want use this change.
Testing for PrimeNG 8.0.0:
#Directive({
selector: 'p-dropdown',
})
export class DropdownDirective implements OnInit, OnDestroy {
readonly KEY_DOWN_EVENT: string = 'keydown';
readonly FOCUS_IN_EVENT: string = 'focusin';
readonly TABINDEX_ATTRIBUTE: string = 'tabindex';
readonly LIST_ITEM_SELECTOR: string = 'li';
private focusInSubscription: Subscription = new Subscription();
private subscriptions: Subscription = new Subscription();
private listElementSubscriptions: Subscription[] = [];
private readonly dropdownHtmlElement: HTMLElement;
constructor(private dropdown: Dropdown,
private elementRef: ElementRef) {
this.dropdownHtmlElement = this.elementRef.nativeElement as HTMLElement;
}
ngOnInit(): void {
this.replaceKeyDownAction();
this.subscribeToDropdownShowEvent();
this.subscribeToDropdownHideEvent();
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe();
}
private subscribeToDropdownShowEvent() {
this.subscriptions.add(
this.dropdown.onShow.subscribe(() => {
this.updateElementsList();
this.subscribeToFocusInEvent();
}),
);
}
private subscribeToDropdownHideEvent() {
this.subscriptions.add(
this.dropdown.onHide.subscribe(() => {
this.unsubscribeFromFocusInEvent();
this.unsubscribeFromListElementsKeyDownEvents();
}),
);
}
private updateElementsList() {
const listElements = this.dropdownHtmlElement.querySelectorAll<HTMLLIElement>(this.LIST_ITEM_SELECTOR);
listElements.forEach((listElement: HTMLLIElement) => {
this.subscribeToListElementKeyDownEvent(listElement);
listElement.setAttribute(this.TABINDEX_ATTRIBUTE, '0');
});
}
private subscribeToListElementKeyDownEvent(listElement: HTMLLIElement) {
this.listElementSubscriptions.push(
fromEvent(listElement, this.KEY_DOWN_EVENT)
.pipe(filter((event: KeyboardEvent) => event.key === KEYBOARD_KEY.ENTER))
.subscribe(() => {
// Simulation of mouse click of list element (trigger with (click) event in p-dropdownItem component which is child element of p-dropdown)
listElement.click();
}),
);
}
private unsubscribeFromListElementsKeyDownEvents() {
this.listElementSubscriptions.forEach((singleSubscription: Subscription) => singleSubscription.unsubscribe());
this.listElementSubscriptions = [];
}
private subscribeToFocusInEvent() {
this.focusInSubscription = fromEvent(document, this.FOCUS_IN_EVENT).subscribe(({target}) => {
// Situation when focus element is outside dropdown component
if (!this.dropdownHtmlElement.contains(target as HTMLElement)) {
this.dropdown.hide();
}
});
}
private unsubscribeFromFocusInEvent() {
this.focusInSubscription.unsubscribe();
}
/**
* Overwrite default onKeydown method from PrimeNG dropdown component
*/
private replaceKeyDownAction() {
const onKeyDownOriginFn = this.dropdown.onKeydown.bind(this.dropdown);
this.dropdown.onKeydown = (event: KeyboardEvent, search: boolean) => {
if (event.which === 9) {
// Napisuję domyślne zachowanie tabulatora zdefiniowanego w klasie komponentu Dropdown z biblioteki PrimeNG
} else {
onKeyDownOriginFn(event, search);
}
}
}
}

Ionic2 view doesn't bind with observable

I have the following component class that receives an observable from a service. I want my component to unsubscribe when destroyed. This is the code is working and the view gets updated (the view renders different content depending on the value of state with *ngIf):
#Component({
templateUrl: 'my-widget.component.html',
selector: 'my-widget'
})
export class MyWidgetComponent implements OnInit, OnDestroy {
// Enum type variable to keep state
state: MyWidgetStateType;
// This is needed to be able to use the enum type in the template
private myWidgetStateType = MyWidgetStateType;
// Keep a reference to the observable to unsubscribe from it
private sub: Subscriber<any>;
constructor(public myWidgetService: MyWidgetService) {
}
ngOnInit(): void {
this.sub = Subscriber.create((state: MyWidgetStateType) => {
console.log('Widget state: ' + MyWidgetStateType[state]);
this.state = state;
});
this.myWidgetService.getState().subscribe(this.sub);
}
ngOnDestroy(): void {
// Unsubscribe here
this.sub.unsubscribe();
}
}
The state type is an enum with 3 states:
export enum MyWidgetStateType {
Enabled,
Dormant,
Locked
};
And the view my-widget.component.html:
<h2>My Widget</h2>
<div *ngIf="state === myWidgetStateType.Enabled">
<p>Enabled</p>
</div>
<div *ngIf="state === myWidgetStateType.Dormant">
<p>Dormant</p>
</div>
<div *ngIf="state === myWidgetStateType.Locked">
<p>Locked</p>
</div>
However, if I try to extract the call in the subscriber to a private function, the console.log method works but the view doesn't render any content according to the state (the <h2> title is rendered but there is no <div> element):
#Component({
templateUrl: 'my-widget.component.html',
selector: 'my-widget'
})
export class MyWidgetComponent implements OnInit, OnDestroy {
// Enum type variable to keep state
state: MyWidgetStateType;
// This is needed to be able to use the enum type in the template
private myWidgetStateType = MyWidgetStateType;
// Keep a reference to the observable to unsubscribe from it
private sub: Subscriber<any>;
constructor(public myWidgetService: MyWidgetService) {
}
ngOnInit(): void {
this.sub = Subscriber.create(this.onStateChange);
this.myWidgetService.getState().subscribe(this.sub);
}
ngOnDestroy(): void {
// Unsubscribe here
this.sub.unsubscribe();
}
private onStateChange(state: MyWidgetStateType): void {
console.log('Widget state: ' + MyWidgetStateType[state]);
this.state = state;
}
}
The question is: why if the onStateChange function gets called, the view never gets the value update?
Of course this is not blocking my work because I have the solution above, but I'm just curious.

Why does :host(:hover) not work here?

I am not able to figure out how :host(:hover) works on child custom elements of a parent custom element. Can someone explain how to make this work?
todoitem.html
<!-- import polymer-element's definition -->
<link rel="import" href="packages/polymer/polymer.html">
<polymer-element name="my-li" extends="li">
<style>
:host(:hover){
color: red;
}
</style>
<template>
<content></content>
</template>
<script type="application/dart" src="todoitem.dart"></script>
</polymer-element>
todoitem.dart
import 'package:polymer/polymer.dart';
import "dart:html" as html;
import 'package:logging/logging.dart';
final Logger _widgetlogger = new Logger("todo.item");
#CustomTag('my-li')
class MyListElement extends html.LIElement with Polymer, Observable {
factory MyListElement() => new html.Element.tag('li', 'my-li');
MyListElement.created() : super.created() {
polymerCreated();
}
#override
void attached() {
super.attached();
_widgetlogger.info("todoitem attached");
}
#override
void detached() {
super.detached();
_widgetlogger.info("todoitem detached");
}
}
todowidget.html
<!-- import polymer-element's definition -->
<link rel="import" href="packages/polymer/polymer.html">
<link rel="import" href="todoitem.html">
<polymer-element name="todo-widget" attributes="title">
<template>
<style>
:host(.colored){
color: blue;
}
</style>
<div>
<h1>{{title}}</h1>
<div>
<input id="inputBox" placeholder="Enter Todo item" on-change="{{addToList}}">
<button id="deleteButton" on-click="{{deleteAll}}">Delete All</button>
</div>
<ul id="todolist">
</ul>
</div>
</template>
<script type="application/dart" src="todowidget.dart"></script>
</polymer-element>
Corresponding Dart Script
import 'package:polymer/polymer.dart';
import "dart:html" as html;
import "todoitem.dart";
import 'package:logging/logging.dart';
final Logger _widgetlogger = new Logger("todo.widget");
#CustomTag('todo-widget')
class TodoWidget extends PolymerElement {
#published String title;
html.InputElement todoInput;
// html.ButtonElement deleteButton;
html.UListElement todolist;
#override
void attached() {
super.attached();
todolist = $["todolist"];
todoInput = $["inputBox"];
}
TodoWidget.created() : super.created() {
//This can go into template!!!
if (title == null) {
title = "My Todo App";
}
;
}
void deleteAll(html.Event e, var detail, html.Node target) {
_widgetlogger.info("All items deleted");
todolist.children.clear();
// print("Clicked");
}
void addToList(html.Event e, var detail, html.Node target) {
_widgetlogger.info("Item added");
MyListElement li = new MyListElement();
li
..text = todoInput.value
..classes.add("todoitem")
..onClick.listen((e) => e.target.remove());
todolist.children.add(li);
todoInput.value = "";
}
}
Upon running I see no hovering effect. How can I fix this?
I guess the problem is that the style tag is outside the <template> tag. It should be inside. I played around with your code (same as in your previous question) and moved the style inside the <template> tag without knowing that I was deviating from the code you posted in your question (I built the element from scratch instead copying the code from your question).

Resources