Clarifying the status of custom attributes in HTML Custom Elements in WebComponents - web-component

Until recently, whenever I've needed a custom attribute in my HTML, I've always used an HTML5 data-* custom attribute.
Having recently started experimenting with WebComponents and, specifically, Custom Elements, I have started thinking in terms of custom attributes which are not HTML5 data-* custom attributes.
Before inadvertently adopting any non-recommended practices, I would like to clarify the following...
In the list below we have 4 elements:
Element i is a standard element with a data-* attribute
Element ii is a standard element with a custom attribute
Element iii is a custom element with a data-* attribute
Element iv is a custom element with a custom attribute
const toggleDataAttribute = (e) => {
e.target.dataset.inverted = (e.target.dataset.inverted === 'true') ? 'false' : 'true';
}
const toggleCustomAttribute = (e) => {
if (e.target.getAttribute('inverted') === 'true') {
e.target.setAttribute('inverted', 'false');
}
else {
e.target.setAttribute('inverted', 'true');
}
}
const toggleInvert = (e) => {
if (e.target.dataset.inverted) {
toggleDataAttribute(e);
}
else {
toggleCustomAttribute(e);
}
}
// Attach click event TO <div> elements
let divs = [...document.getElementsByTagName('div')];
divs.forEach((div) => div.addEventListener('click', toggleInvert, false));
// Attach click event TO <my-circle> elements
let myCircles = [...document.getElementsByTagName('my-circle')];
myCircles.forEach((myCircle) => myCircle.addEventListener('click', toggleInvert, false));
// Define <my-circle> element
class myCircle extends HTMLElement {
constructor() {
super();
this.root = this.attachShadow({mode: "open"});
}
connectedCallback() {
this.root.appendChild(document.createElement('slot'));
}
}
customElements.define('my-circle', myCircle);
aside {
position: absolute;
top: 0;
right: 0;
width: 280px;
line-height: 24px;
}
div {
float: left;
margin: 0 12px 12px 0;
width: 80px;
height: 80px;
line-height: 80px;
text-align: center;
font-size: 36px;
border-radius: 50%;
cursor: pointer;
}
my-circle {
display: block;
float: left;
margin: 0 12px 12px 0;
width: 80px;
height: 80px;
line-height: 80px;
text-align: center;
font-size: 36px;
background: radial-gradient(#fff, #000);
border-radius: 50%;
cursor: pointer;
}
my-circle:first-of-type {
clear: left;
}
div:nth-of-type(1) {
background: radial-gradient(rgb(255, 255, 0), rgb(255, 0, 0));
}
div:nth-of-type(2) {
background: radial-gradient(rgb(255, 255, 0), rgb(0, 163, 0));
}
my-circle:nth-of-type(1) {
background: radial-gradient(rgb(255, 255, 0), rgb(223, 163, 0));
}
my-circle:nth-of-type(2) {
background: radial-gradient(rgb(255, 127, 127), rgb(255, 0, 0));
}
div[data-inverted="true"],
div[inverted="true"],
my-circle[data-inverted="true"],
my-circle[inverted="true"] {
filter: hue-rotate(180deg);
}
<div data-inverted="false">i</div>
<div inverted="false">ii</div>
<my-circle data-inverted="false">iii</my-circle>
<my-circle inverted="false">iv</my-circle>
<aside>
<p><strong>Click</strong> on each of the circles on the left to invert their backgrounds.</p>
</aside>
Although the set up above works technically, which of the following is true:
A) Custom attributes may be used universally, in standard elements and custom elements.
Conclusion: Elements i, ii, iii & iv are all valid
B) Custom attributes may only be used in custom elements. They are invalid elsewhere.
Conclusion: Elements i, iii & iv are valid, while ii is invalid
C) Data-* attributes are for standard elements, custom attributes are for custom elements.
Conclusion: Elements i & iv are valid, while ii & iii are invalid
D) Custom attributes are not even a thing. Where did you get this idea from?
Conclusion: Elements i & iii are valid, while ii & iv are invalid
Added:
To illustrate my question above, I'd like to give an example of where custom attributes appear not to be valid:
Go to: https://validator.w3.org/nu/#textarea
Select text input
Enter:
<!DOCTYPE html>
<html lang="">
<head>
<title>Test</title>
</head>
<body>
<div data-inverted="false">i</div>
<div inverted="false">ii</div>
</body>
</html>
Check the markup
The validator returns the error:
Error: Attribute inverted not allowed on element div at this point.
From line 10, column 1; to line 10, column 22
i</div>↩↩<div inverted="false">ii</di
Though... I'm not sure if the tool at https://validator.w3.org/nu/ is outdated and / or abandoned and the Error returned should no longer be regarded as an error in 2020 (?)

All 4 usages work, so why should they be invalid?
data- prefix gives the added bonus they are available in element.dataset.
-- Attributes are Attributes -- , nothing special in the Custom Elements API,
apart from observedAttributes(). Yes, you can use data-* attributes there to.
note
class myCircle extends HTMLElement {
constructor() {
super();
this.root = this.attachShadow({mode: "open"});
}
connectedCallback() {
this.root.appendChild(document.createElement('slot'));
}
}
can be written as:
class myCircle extends HTMLElement {
constructor() {
super()
.attachShadow({mode: "open"})
.append(document.createElement('slot'));
}
}
because super() returns 'this'
and attachShadow both sets and returns this.shadowRoot for free
you are not doing anything with appendChild() return value, so append() (which can take multiple parameters) is enough.
Also note there is a toggleAttribute method.
https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append
https://developer.mozilla.org/en-US/docs/Web/API/Element/toggleAttribute

Related

CodeMirror Line-Break doesn't add line number - Angular

I'm using code mirror from ngx-codemirror. I want to split the line when it fits to the width of the parent. I have found some solutions to split the like using,
lineWrapping: true
and in styles
.CodeMirror-wrap pre {
word-break: break-word;
}
Using this I was able to split the line but I need to show the line number too.
The line number is not shown for the line that was just split.
This is the stackblitz link to my issue : code-mirror-line-break-issue
Screenshot :
Please help me with this.
This is not feasible using Code Mirror options, as this is something that is a bit counter intuitive that is rarely (ever?) wanted.
Like I said in my comment, say 2 persons discussing on a phone/web chat about a piece of code/json. They will not see the same thing when one mentions a line number to the other if they have different windows/screen sizes
Solution
As a hack, you can create your own elements representing line numbers and place them over the default line numbers.
Here is the stackblitz demo
Note: This a a very basic example. If you change code mirror settings (font size, gutters,...), you might need to tweak the css or do more calculation based on these settings.
component.html
<div class='codeMirrorContainer'>
<ngx-codemirror
#codeMirror
[options]="codeMirrorOptions"
[(ngModel)]="codeObj"
></ngx-codemirror>
<ul class='lineContainer' [style.top.px]="-topPosition">
<li [style.width.px]='lineWidth' *ngFor="let line of lines">{{line}}</li>
</ul>
</div>
component.css
li
{
height: 19px;
list-style: none;
}
.codeMirrorContainer
{
position:relative;
overflow: hidden;
}
.lineContainer
{
position:absolute;
top:0;
left:0;
margin: 0;
padding: 5px 0 0 0;
text-align: center;
}
::ng-deep .CodeMirror-linenumber
{
visibility: hidden; /* Hides default line numbers */
}
component.ts
export class AppComponent
{
#ViewChild('codeMirror') codeMirrorCmpt: CodemirrorComponent;
private lineHeight: number;
public lineWidth;
public topPosition: number;
public lines = [];
codeMirrorOptions: any = ....;
codeObj :any = ...;
constructor(private cdr: ChangeDetectorRef)
{
}
ngAfterViewInit()
{
this.codeMirrorCmpt.codeMirror.on('refresh', () => this.refreshLines());
this.codeMirrorCmpt.codeMirror.on('scroll', () => this.refreshLines());
setTimeout(() => this.refreshLines(), 500)
}
refreshLines()
{
let editor = this.codeMirrorCmpt.codeMirror;
let height = editor.doc.height;
this.lineHeight = editor.display.cachedTextHeight ? editor.display.cachedTextHeight : this.lineHeight;
if (!this.lineHeight)
{
return;
}
let nbLines = Math.round(height / this.lineHeight);
this.lines = Array(nbLines).fill(0).map((v, idx) => idx + 1);
this.lineWidth = editor.display.lineNumWidth;
this.topPosition = document.querySelector('.CodeMirror-scroll').scrollTop;
this.cdr.detectChanges();
}
}

How do I make a Material toolbar opaque on scroll and transparent at start?

I'm currently trying to familiarise myself with angular. I'm using angular material and I'm looking to make the material toolbar sticky and opaque on scroll and, transparent with toolbar text still visible when at the very top of the page. Everything I've searched for so far involved javascript or jquery. How do I go about it in angular 8 precisely?
This is my HTML & CSS respectively:
<mat-toolbar color="primary">
<a mat-button [routerLink]="['home']" >
<h1>PETER<span class="light">CONSTRUCTION</span></h1>
</a>
<span class="spacer"></span>
<a mat-button [routerLink]="['home']" routerLinkActive="active" >HOME</a>
<a mat-button [routerLink]="['about']" routerLinkActive="active">ABOUT</a>
<a mat-button [routerLink]="['contact']" routerLinkActive="active">CONTACT</a>
</mat-toolbar>
mat-toolbar {
position: absolute;
z-index: 1;
overflow-x: auto;
background-color: #c3cfd2;
}
mat-toolbar-row {
justify-content:space-between;
}
.spacer {
flex: 1 1 auto;
}
a.active {
background-color: rgba(0,0,0, 0.3);
}
h1 {
margin: 0;
color: black;
}
h1 .light {
font-weight: 100;
}
/*.x-bar.x-bar-absolute{*/
/* background-color: hsla(276, 6%, 63%, 0.15) !important;*/
/* transition: none !important;*/
/*}*/
/*.x-bar.x-bar-fixed{*/
/* background-color: hsla(276, 6%, 63%, 1) !important;*/
/*}*/
/*.x-bar [class^="x-bg"] {*/
/* background-color: transparent !important;*/
/*}*/
/*.x-bar.x-bar-absolute .hm5.x-menu > li > .x-anchor .x-anchor-text-primary {*/
/* color: #fff;*/
/*}*/
/*.x-bar.x-bar-fixed .hm5.x-menu > li > .x-anchor .x-anchor-text-primary {*/
/* color: #000;*/
/*}*/
There are multiple ways of achieving this, but since you're already using #angular/material, you can take advantage of the #angular/cdk and it's ScrollDispatchModule (see docs).
It allows you for easy and clean observing of scroll events for registered elements, outside of the NgZone, meaning it will have small impact on the performance.
See the example stackblitz:
https://stackblitz.com/edit/angular-npdbtp
First, you need to import ScrollDispatchModule and register provider for ScrollDispatcher:
import {ScrollDispatchModule, ScrollDispatcher} from '#angular/cdk/scrolling';
#NgModule({
imports: [
(other imports)
ScrollDispatchModule
],
providers: [ScrollDispatcher]
})
export class AppModule {}
Then in your template you can mark an html element with the cdkScrollable directive. This will automatically register it in the ScrollDispatcher.
You can also bind component's style (e.g. opacity) to a property defined in your component:
<div class="scroll-wrapper" cdkScrollable>
<mat-toolbar class="sticky-toolbar" [style.opacity]="opacity">My App</mat-toolbar>
<div>content</div>
</div>
You can make html element sticky using the display: sticky together with top: 0:
.sticky-toolbar {
position: sticky;
top: 0px;
}
Then you will need to inject the ScrollDispatcher and NgZone into your component and define opacity property:
opacity = 1;
constructor(
private scrollDispatcher: ScrollDispatcher,
private zone: NgZone
) {}
Then you can subscribe to scrolled events of the ScrollDispatcher. Those are emitted for all the registered components. You can also register to scroll events of a single element - refer to the docs if needed by.
ngOnInit(): void {
this.scrollDispatcher.scrolled().subscribe((event: CdkScrollable) => {
const scroll = event.measureScrollOffset("top");
let newOpacity = this.opacity;
if (scroll > 0) {
newOpacity = 0.75;
} else {
newOpacity = 1;
}
if (newOpacity !== this.opacity) {
this.zone.run(() => {
this.opacity = newOpacity;
});
}
});
}
The ScrollDispatcher runs outside of NgZone, meaning it will not run change detection in the whole application. This allows for better performance, and it's why we're also injecting NgZone and running the property change inside the zone - this calls the proper change detection along the tree of components.

How to pass parameter from Angular to CSS in HTML table

I have HTML table in Angular web app.
Get the data from the database via service and display it as text.
One of the columns is Status, so I need to present that variable not as text,
but as a circle of certain color, so if that cell value is "green",
it should show as green circle.
Here is what I have.
CSS (part of Component Styles):
:host ::ng-deep td.circle {
text-align: center;
font-size: 0;
background-color: RED; <--- need to pass param here
}
I define that column as:
.circle {
border-radius: 50%;
height: 24px;
width: 24px;
}
HTML:
<app-table [attrs]="serviceapi" [monthSelected]="monthSelected" ></app-table>
TS for that table:
In constructor:
this.data = {
headers: [],
rows: []
};
ngOnInit() {
this.tableMetricsService.getTableMetrics(
JSON.parse(this.attrs).api,
this.monthSelected
).subscribe(response => {
this.data = response;
}
);
}
Any idea how to "translate" cell value into the CSS background-color ?
TIA,
Oleg.
You could put a data attribute on the html and write a corresponding selector:
[data-status="green"] {
background: green;
}
[data-status="red"] {
background: red;
}
<div data-status="green">Green</div>
<div data-status="red">Red</div>
But I don't see any advantage to this approach over using a regular css class:
.green {
background: green;
}
.red {
background: red;
}
<div class="green">Green</div>
<div class="red">Red</div>
Here is what worked for me:
<div *ngIf="cell.styleClass == 'circle'" [ngStyle]="{'background-color': cell.value}" [ngClass]="cell.styleClass">
</div>

Relate CSS variables to each other in context

Background: trying to create an element for easily embedding Font Awesome 5.10.2 Duotone icons into any piece of HTML.
This icon element uses HTML attributes which should map to a specific icon where the mapping is purely controlled by the CSS author.
<x pay></x> <!-- <- icon value for pay should be customizable by CSS author -->
Below is my solution but I wonder ...
Can one reduce
x {
position: relative;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-style: normal;
font-variant: normal;
text-rendering: auto;
white-space: nowrap;
font-family: var(--fa-5-d);
font-weight: var(--fa-d);
background: var(--x-background);
line-height: 1em !important;
}
x::after { position: absolute; left: 0; bottom: 0; }
x::before { color: var(--fa-primary-color, inherit); opacity: 1; opacity: var(--fa-primary-opacity, 1.0); }
x::after { color: var(--fa-secondary-color, inherit); opacity: var(--fa-secondary-opacity, 0.4); }
x:before { --fa-credit-card: "\f09d"; }
x:after { --fa-credit-card: "\10f09d"; }
<x pay></x>
this ↓
x[pay]:before,
x[pay]:after { content: var(--fa-credit-card); }
to this ↓ (avoiding x[pay]:before, x[pay]:after repetition)
x[pay] { --content: var(--fa-credit-card); }
in essence
set a CSS variable once on a parent to a value v
that diverges into different child values v₁ and v₂ related to v?
?
A convenience improvement using web components / custom elements:
<link href='//cdn.blue/{fa-5.10.2}/css/all.css' rel=stylesheet>
<link href='//cdn.blue/{fa+}/var.css' rel=stylesheet>
<link href='//cdn.blue/{fa+}/x-i.css' rel=stylesheet>
<script src='//cdn.blue/<shin>/shin-element.js'></script>
<script>
class XI extends ShinElement {
constructor() {
super(`<style></style>`);
ShinElement.IPA(this, 'jsUpdate', { a: 'js-update', t: ShinElement.Number0 });
}
connectedCallback() { XI.css(this); }
static css(t) {
const c = getComputedStyle(t);
const i = c.getPropertyValue('--i').trim();
const s = t._.QS("style");
s.textContent = `:host:before, :host:after { content: var(${i}); }`;
}
static get observedAttributes() { return [ 'js-update' ]; }
attributeChangedCallback(a, o, n) {
switch (a) {
case "js-update":
const u = this.jsUpdate;
if (u > 0) this._jsu = setInterval(XI.css, u, this);
else { clearInterval(this._jsu); delete this._jsu; }
break;
}
}
}
XI.define();
</script>
<style>x-i[pay] { --i: --fa-user-edit; }</style>
<x-i pay js-update=200 id=x></x-i>
https://cdn.blue/<shin>/docs/ShinElement.html
https://codepen.io/cetinsert/pen/QWLZgwZ?editors=1000
The need for js-update (HTML), jsUpdate (JS) for doing live CSS edits is unfortunate.
x.jsUpdate = 0; // to disable getComputedStyle updates
Thus I still wonder if there is a better solution - be it CSS-only or using web components.

Input effect on keyboard tab -> focus, but NOT on click

When a user 'tabs over' to an input, I want the focus effect to be normally displayed, but on click, I don't want it to be visible.
User hits tab, now focussed on toggle button, I would like the toggle button to have slight glowing outline, which I'm currently able to do.
Now,
User clicks on the toggle button or it's associated label, toggle changes as usual,
BUT, I want the glow to never appear in the first place, or to disappear as quickly as possible.
I know about .blur(), and right now I'm having to use a setTimeout for a lazy fix, but I'd like to know if there's a better way to accomplish this, or if there's possibly a CSS only solution
I think a lot of front-end developers struggle to find a balance between aesthetics and the best-practices for accessibility. This seems like a great compromise.
Here's how I do it. The idea is to toggle outlining on when the user uses the tab key and turn it back off when they click.
JS
document.addEventListener('keydown', function(e) {
if (e.keyCode === 9) {
$('body').addClass('show-focus-outlines');
}
});
document.addEventListener('click', function(e) {
$('body').removeClass('show-focus-outlines');
});
Styles
body:not(.show-focus-outlines) button:focus,
body:not(.show-focus-outlines) [tabindex]:focus {
outline: none;
}
I'm currently doing something similar for my company. Unfortunately you must use JavaScript since CSS doesn't support this use case.
Here's what I've done.
var btns = document.querySelectorAll('button');
var onMouseDown = function (evt) {
evt.target.dataset.pressed = 'true';
};
var onMouseUp = function (evt) {
evt.target.dataset.pressed = 'false';
};
var onFocus = function (evt) {
var element = evt.target;
if (element.dataset.pressed !== 'true') {
element.classList.add('focus');
}
};
var onBlur = function (evt) {
evt.target.classList.remove('focus');
};
for(var i = 0, l = btns.length; i < l; i++) {
btns[i].addEventListener('mousedown', onMouseDown);
btns[i].addEventListener('mouseup', onMouseUp);
btns[i].addEventListener('focus', onFocus);
btns[i].addEventListener('blur', onBlur);
}
* { box-sizing: border-box; }
body { background-color: white; }
button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
min-width: 100px;
margin: 0 1px;
padding: 12px 10px;
font-size: 15px;
color: white;
background-color: #646e7c;
border: none;
border-radius: 5px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.2);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
button:focus { outline: none; }
button:active {
-webkit-transform: translateY(1px);
-moz-transform: translateY(1px);
transform: translateY(1px);
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.2);
}
button.focus {
font-weight: bold;
}
button.primary { background-color: #2093d0; }
button.success { background-color: #71a842; }
button.danger { background-color: #ef4448; }
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button>Default</button>
<button class="primary">Primary</button>
<button class="success">Success</button>
<button class="danger">Danger</button>
</body>
</html>
Basically instead of relying on browser's native focus I add/remove a focus class on my button depending on the situation.
If you use the what-input.js plugin you can apply styles specifically for keyboard users. You can use the following code to highlight a button that has been tabbed to. I've found what-input to be a reliable plugin (comes bundled with Zurb Foundation) and is currently regularly maintained.
// scss
body[data-whatinput="keyboard"] {
button {
&:focus {
// other highlight code here
box-shadow: 0 0 5px rgba(81, 203, 238, 1);
}
}
}
or
/* vanilla css */
body[data-whatinput="keyboard"] button:focus {
box-shadow: 0 0 5px rgba(81, 203, 238, 1);
}

Resources