Style web component within shadow dom, depending on its parent - css

I am working on a component for uploading files. I have made two web components (wc):
wc-uploader the parent with the select files button.
wc-upload the children that are added to wc-uploader as file are added.
If the parent (wc-uploader) has the readonly or disabled attribute, I wish to style the wc-upload items differently.
I think the styling should be within the wc-upload component as it pertains to it.
This is the selector I tried within the wc-upload template but it does not work. I am guess it can't see beyond its shadow root.
wc-uploader[readonly] :host #close { /* here host = wc-uploader */
opacity: 0.5;
}
How would one style this element depending on its parent.
E.g. like if a select item is disabled, then it's option children are disabled too.

For loose coupling, so it doesn't matter when or where children Web Components are attached:
Make the children listen:
this.closest("wc-uploader").addEventListener("close",(evt)=>{
let parent = evt.detail; //evt.target could do
if parent.hasAttribute("close") ...
else ...
});
Then the parent reports its state:
attributeChangedCallback(name,oldValue,newValue){
if(name=="close" || name=="readonly" || name=="disabled"){
this.dispatchEvent(new CustomEvent(name, {
bubbles: false, // Event stays at wc-uploader
detail: this // wc-uploader
}));
}
}
If your children are deeper down in shadowRoots you need:
Custom Element getRootNode.closest() function crossing multiple (parent) shadowDOM boundaries
Or use document. as your "Event Bus", but then you have to be careful with your Event-names.
Be aware addEventListener attaches a listener outside the Web Component scope; so it is not garbage collected when the Component is removed; your task to remove them in the disconnectedCallback

Related

How to use components in different ways (React)

I want to use simple components in different way and different ui rendering
For example a dropdown rendering a list may have several ui according to the page or context (=> padding, margins, font size and other css properties might change)
should I:
implement it by overwriting in the parent component (target css properties of the child component and apply them my css needs - at cost that if change happens in the child component like change in classname or what might break the parent design)
Pass flags to the component to handle those design and at cost that each component handle the design of each parent
There are different approaches to this and everybody has his own preferences.
I usually solve this by supporting the className property. The class is accepted as a prop and applied to the root. So it is easy to change things like outer margins or the background-color. I usually discourage modifications of deeply nested elements.
Example:
import classnames from 'clsx';
import style from './button.module.scss';
export const Button = ({ content, onClick, className }) => {
return (
<div
className={classnames(style.buttonRoot, className)}
onClick={onClick}>
{content}
</div>
);
};
and if I want to modify it anywhere I can do it thus:
import { Button } from './Button';
import style from './productView.module.scss';
// ...
<Button content={'Show products'} className={style.showProdButton} onClick={showProd} />
and
.show-prod-button {
background-color: #562873;
margin-left: 32px;
}

Does CSS change the DOM?

I was wondering if CSS changes the DOM.
The reason I am asking, is that whenever I change an Element with CSS, I don't see it's value changed in the "element".style properties.
No, CSS does not change the DOM.
No. CSS does not change the DOM.
Nor content injected using :after or :before alter the DOM.
Actually... there are a few cases where CSS can change the DOM, but it's a bit far-stretched, as it won't change the DOM-tree structure, except in one yet even more far stretched case...
There is a being rendered definition in the HTML specs that does impact the behavior of the DOM in some cases, based on CSS computed styles.
For instance,
an HTMLImageElement can have its width and height IDL attributes value change whether it is being rendered or not:
onload = (evt) => {
console.log( 'rendered', document.getElementById( 'disp' ).width );
console.log( 'not rendered', document.getElementById( 'no-disp' ).width );
}
img { width: 100px; }
#no-disp { display: none; }
<img id="disp" src="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png">
<img id="no-disp" src="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png">
Elements that are not being rendered can not be focusable elements:
document.getElementById( 'rendered' ).focus();
console.log( document.activeElement ); // <input id="rendered">
document.getElementById( 'rendered' ).blur();
document.getElementById( 'not-rendered' ).focus();
console.log( document.activeElement ); // <body>
#not-rendered {
display: none;
}
<input id="rendered">
<input id="not-rendered">
And the one case where the DOM tree is modified, concerns the DOM tree of an inner Document: When an <object> or an <embed> element has its style set to display:none, per specs, its inner Document should be reloaded:
Whenever one of the following conditions occur
[...]
the element changes from being rendered to not being rendered, or vice versa,
...the user agent must queue an element task on the DOM manipulation task source given the object element to run the following steps to (re)determine what the object element represents.
So this means that simply switching the being rendered state of such an <object> or <embed> element is supposed to reload entirely its inner Document, which means also its DOM tree.
Now, only Safari behaves like that, Firefox never implemented that behavior, and Chrome did recently change their to match FF's one, against the specs.
For Safari users, here is a fiddle demonstrating it.

How would I apply Material-UI managed styles to non-material-ui, non-react elements?

I have an application where I'm using Material UI and its theme provider (using JSS).
I'm now incorporating fullcalendar-react, which isn't really a fully fledged React library - it's just a thin React component wrapper around the original fullcalendar code.
That is to say, that I don't have access to things like render props to control how it styles its elements.
It does however, give you access to the DOM elements directly, via a callback that is called when it renders them (eg. the eventRender method).
Here's a basic demo sandbox.
Now what I'm wanting to do is make Full Calendar components (eg, the buttons) share the same look and feel as the rest of my application.
One way to do this, is that I could manually override all of the styles by looking at the class names it's using and implementing the style accordingly.
Or - I could implement a Bootstrap theme - as suggested in their documentation.
But the problem with either of these solutions, is that that:
It would be a lot of work
I would have synchronisation problems, if I made changes to my MUI theme and forgot to update the calendar theme they would look different.
What I would like to do is either:
Magically convert the MUI theme to a Bootstrap theme.
Or create a mapping between MUI class names and the calendar class names, something like:
.fc-button = .MuiButtonBase-root.MuiButton-root.MuiButton-contained
.fc-button-primary= .MuiButton-containedPrimary
I wouldn't mind having to massage the selectors etc to make it work (ie. For example - MUI Buttons have two internal spans, whereas Full Calendar have just one). It's mostly about when I change the theme - don't want to have to change it in two places.
Using something like Sass with its #extend syntax would is what I have in mind. I could create the full-calendar CSS with Sass easily enough - but how would Sass get access to the MuiTheme?
Perhaps I could take the opposite approach - tell MUI 'Hey these class names here should be styled like these MUI classes'.
Any concrete suggestions on how I would solve this?
Here is my suggestion (obviously, it's not straight forward). Take the styles from the MUI theme and generate style tag based on it using react-helmet. To do it event nicely, I created a "wrapper" component that do the map. I implemented only the primary rule but it can be extended to all the others.
This way, any change you will do in the theme will affect the mapped selectors too.
import React from "react";
import { Helmet } from "react-helmet";
export function MuiAdapter({ theme }) {
if (!theme.palette) {
return <></>;
}
return (
<Helmet>
<style type="text/css">{`
.fc-button-primary {
background: ${theme.palette.primary.main}
}
/* more styles go here */
`}</style>
</Helmet>
);
}
And the use of the adapter
<MuiAdapter theme={theme} />
Working demo: https://codesandbox.io/s/reverent-mccarthy-3o856
You could create a mapping between MUI class names and the calendar class names by going through ref's. It's possible that this is not what some would call "best practice"...but it's a solution :). Note that I updated your component from a functional component to a class component, but you could accomplish this with hooks in a functional component.
Add refs
Add a ref to the MUI element you want to set as a reference, in your case the Button.
<Button
color="primary"
variant="contained"
ref={x => {
this.primaryBtn = x;
}}
>
And a ref to a wrapping div around the component you want to map to. You can't add it directly to the component since that wouldn't give us access to children.
<div
ref={x => {
this.fullCal = x;
}}
>
<FullCalendar
...
/>
</div>
Map classes
From componentDidMount() add whatever logic you need to target the correct DOM node (for your case, I added logic for type and matchingClass). Then run that logic on all FullCalendar DOM nodes and replace the classList on any that match.
componentDidMount() {
this.updatePrimaryBtns();
}
updatePrimaryBtns = () => {
const children = Array.from(this.fullCal.children);
// Options
const type = "BUTTON";
const matchingClass = "fc-button-primary";
this.mapClassToElem(children, type, matchingClass);
};
mapClassToElem = (arr, type, matchingClass) => {
arr.forEach(elem => {
const { tagName, classList } = elem;
// Check for match
if (tagName === type && Array.from(classList).includes(matchingClass)) {
elem.classList = this.primaryBtn.classList.value;
}
// Run on any children
const next = elem.children;
if (next.length > 0) {
this.mapClassToElem(Array.from(next), type, matchingClass);
}
});
};
This is maybe a little heavy handed, but it meets your future proof requirement for when you updated update Material UI. It would also allow you to alter the classList as you pass it to an element, which has obvious benefits.
Caveats
If the 'mapped-to' component (FullCalendar) updated classes on the elements you target (like if it added .is-selected to a current button) or adds new buttons after mounting then you'd have to figure out a way to track the relevant changes and rerun the logic.
I should also mention that (obviously) altering classes might have unintended consequences like a breaking UI and you'll have to figure out how to fix them.
Here's the working sandbox: https://codesandbox.io/s/determined-frog-3loyf

Which CSS selector is used when a DragOver event is detected?

I am creating a custom RowFactory for my TableView to accept drag-and-drop files. I want to update the style of the specific Row when an acceptable DragOver event is detected.
Using :hover obviously won't work because that would apply even if the user is not dragging anything.
The end goal is simply to make it visually clear which row the user is about to drop the items onto.
Is there a selector I can use in my stylesheet to handle this? I could not find anything in the JavaFX CSS Reference Guide.
I can currently work around this by defining my own StyleClass and adding it in the setOnDragOver() method:
setOnDragOver(event -> {
// Determine if the dragged items are files
if (!this.isEmpty() && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.LINK);
this.getStyleClass().add("dragging");
}
});
However, attempting to remove the class when exiting does not seem to work:
setOnDragExited(event -> this.getStyleClass().remove("dragging"));
Edit: I should also clarify that each row may have other styles applied to them (based on several factors) and would want to ADD a style to the row when being dragged over, not replace all the rest)
As mentioned by #kleopatra, working with custom PseudoClass can work for you.
/**
* Interface to keep all custom pseudo classes.
*/
public interface Styles{
/** Dragged pseudo class. */
public static final PseudoClass DRAGGED_PSEUDOCLASS = PseudoClass.getPseudoClass("dragged");
}
In your code:
setOnDragOver(event -> {
if (!this.isEmpty() && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.LINK);
this.pseudoClassStateChanged(Styles.DRAGGED_PSEUDOCLASS,true);
}
});
setOnDragExited(event -> this.pseudoClassStateChanged(Styles.DRAGGED_PSEUDOCLASS,false));
In CSS:
.table-row-cell:dragged{
-fx-background-color:$custom-color;
}

#HostBinding class assignment not respecting CSS :nth-child

I'm running into the following issue which I can't think of a way around:
I have a the following application structure:
AppComponent (access to Redux store)
||
CardStackComponent
||
CardComponent (many cards in a stack)
each CardComponent can have three states: accepted, rejected, or neither, represented by three classes: card--accepted, card--rejected, and card--in-stack.
There are buttons the user can click to accept or reject the topmost CardComponent, which bubbles an event up to AppComponent. AppComponent then uses a reducer to update the value of an accepted and rejected property on the Object that is used to render the CardComponent. In CardComponent I have three #HostBinding decorators which are used to add/remove classes from the CardComponent host element.
#HostBinding('class.card--accepted') accepted: boolean = false;
#HostBinding('class.card--rejected') rejected: boolean = false;
#HostBinding('class.card--in-stack') inStack: boolean = true;
the classes are being added/removed as expected as I hit the buttons to accept/reject the cards, however, there's one strange issue: To allow the cards to stack, I'm using CSS to position the top three cards absolutely on top of eachother, and hide any others. This works perfectly and is achieved using :nth-child
card--in-stack {
&:nth-child(1) {...}
&:nth-child(2) {...}
&:nth-child(3) {...}
&:nth-child(n+4) {...}
}
however, when the top card is accepted, the class card--in-stack is removed and the class card--accepted is added. This means that what was the second card in the stack (card--in-stack:nth-child(2)) should now be card--in-stack:nth-child(1), the one below it should now be card--in-stack:nth-child(2), and so on. Inspecting the second card in the stack (the one under the card that was just accepted), it still has the card--in-stack:nth-child(2) styles being applied, even through it is the first element on the page with class card--in-stack. Is there a way to get the CSS to respect :nth-child when changing classes on a component using the #HostBinding decorator?

Resources