Is it possible to programmatically slot elements in web components? - html

Is it possible to automatically or programmatically slot nested web components or elements of a specific type without having to specify the slot attribute on them?
Consider some structure like this:
<parent-element>
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
<p>Content</p>
</parent-element>
With the <parent-element> having a Shadow DOM like this:
<div id="child-elements">
<slot name="child-elements">
<child-element>Default child</child-element>
</slot>
</div>
<div id="content">
<slot></slot>
</div>
The expected result is:
<parent-element>
<#shadow-root>
<div id="child-elements">
<slot name="child-elements">
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
</slot>
</div>
<div id="content">
<slot>
<p>Content</p>
</slot>
</div>
</parent-element>
In words, I want to enforce that <child-element>s are only allowed within a <parent-element> similar to <td> elements only being allowed within a <tr> element. And I want them to be placed within the <slot name="child-elements"> element. Having to specify a slot attribute on each of them to place them within a specific slot of the <parent-element> seems redundant.
At the same time, the rest of the content within the <parent-element> should automatically be slotted into the second <slot> element.
I've first searched for a way to define this when registering the parent element, though CustomElementRegistry.define() currently only supports extends as option.
Then I thought, maybe there's a function allowing to slot the elements manually, i.e. something like childElement.slot('child-elements'), but that doesn't seem to exist.
I've then tried to achive this programmatically in the constructor of the parent element like this:
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
const childElements = this.getElementsByTagName('child-element');
const childElementSlot = this.shadowRoot.querySelector('[name="child-elements"]');
for (let i = 0; i < childElements.length; i++) {
childElementSlot.appendChild(childElements[i]);
}
}
Though this doesn't move the child elements to the <slot name="child-elements">, so all of them still get slotted in the second <slot> element.

Your unnamed default <slot></slot> will capture all elements not assigned to a named slot;
so a slotchange Event can capture those and force child-element into the correct slot:
customElements.define('parent-element', class extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
this.shadowRoot.addEventListener("slotchange", (evt) => {
if (evt.target.name == "") {// <slot></slot> captures
[...evt.target.assignedElements()]
.filter(el => el.nodeName == 'CHILD-ELEMENT') //process child-elements
.map(el => el.slot = "child-elements"); // force them to their own slot
} else console.log(`SLOT: ${evt.target.name} got:`,evt.target.assignedNodes())
})}});
customElements.define('child-element', class extends HTMLElement {
connectedCallback(parent = this.closest("parent-element")) {
// or check and force slot name here
if (this.parentNode != parent) {
if (parent) parent.append(this); // Child 3 !!!
else console.error(this.innerHTML, "wants a PARENT-ELEMENT!");
}}});
child-element { color: red; display: block; } /* style lightDOM in global CSS! */
<template id=PARENT-ELEMENT>
<style>
:host { display: inline-block; border: 2px solid red; }
::slotted(child-element) { background: lightgreen }
div { border:3px dashed rebeccapurple }
</style>
<div><slot name=child-elements></slot></div>
<slot></slot>
</template>
<parent-element>
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
<b>Content</b>
<div><child-element>Child 3 !!!</child-element></div>
</parent-element>
<child-element>Child 4 !!!</child-element>
Note the logic for processing <child-element> not being a direct child of <parent-element>, you probably want to rewrite this to your own needs

As of recently, yes, you can, by using the assign() method of slot elements. Sadly, Safari doesn't support it yet, but there is a polyfill.

Related

Couldn't use the same classname across components. CSS is not being scoped inside a style component

The problem I am having seems to defeat the very purpose of CSS in JS. I am using styled-compomnents. And when I tried to use a classname that is being used somewhere up in the react tree inside a styled component. The upper component classname styles somehow get applied to the classname I used down (very) the tree.
Steps to reproduce
Render UpperComponent anywhere in a react project.
const StyledContainer = styled.div`
.title {
color: red;
margin-bottom: 32px;
}
`;
const UpperComponent = () => {
return (
<StyledContainer>
<FirstComponent />
<h4 className="title"> text inside upper component </h4>
</StyledContainer>
);
};
const FirstStyledContainer = styled.div``;
const FirstComponent = () => {
return (
<FirstStyledContainer>
<h4 className="title">text inside first component</h4>
<SecondComponent />
</FirstStyledContainer>
);
};
const SecondComponent = () => {
return (
<div>
<h4 className="title">text inside second component</h4>
<ThirdComponent />
</div>
);
};
const ThirdComponent = () => {
return (
<div>
<h4 className="title">text inside second component </h4>
</div>
);
};
Expected Behavior
title classname in the UpperComponent should not affect it's descendants' elements with the same classname. It should be scoped only to <h4 className="title"> text inside upper component </h4>
Actual Behavior
.title { color: red; margin-bottom: 32px; } class styles get applied to all the components inside UpperComponent. title somehow makes it down to ThirdCompoent which is nested inide two components.
Is this expected behavior or am I missing something basic (best practice)?
If you want enforce the scoping - You can remove the class names and/or let "styled component" name them (generates a random hash class name) by creating a TitleStyle and attach to the title div (class name "title" can be removed). This should scope to that title only then. Right ?
Another alternative
Yes the FirstComponent and SecondComponent (etc) will catch the css rule from the top. This is the expected result for me. Its not like when we do this below !
<div style = {{color:"red"}}>Test</div>
This would apply the css inline to that div only.
I would slightly change the names of the title classes like so
const StyledContainer = styled.div`
.title {
color: red;
margin-bottom: 32px;
&.secondary { color: pink; }
&.thirdly { color: yellow; }
}
`;
const UpperComponent = () => {
return (
<StyledContainer>
<FirstComponent />
<h4 className="title"> text inside upper component </h4>
</StyledContainer>
);
};
const SecondComponent = () => {
return (
<div>
<h4 className="title secondary">text inside second component</h4>
<ThirdComponent />
</div>
);
};
const ThirdComponent = () => {
return (
<div>
<h4 className="title thirdly">text inside second component </h4>
</div>
);
};
The & is a SCSS operator and works fine with styled components.
CSS is more benifical to behave this way as passing css rules down is more effecient. Work with this effeciency ! You want to create site wide CSS patterns, try avoid specific targeting unless your sure its required (Which should be not too common).
What I do moslty is, created a styled component for the react component, so one per react components to handle all css/scss in that react component.
Daniel
This is working as it should. You're selecting all the .titles in that styled-component.
In the end, styled-components just generate a unique class name for every styled-component you made. So the rules of CSS still work there.
You can
You can select only the direct descendant .title.
const StyledContainer = styled.div`
>.title {
// rules...
}
`
Change the class name to something more specific.
Nest the CSS rule on the parent. So instead of this,
const StyledContainer = styled.div`
.title {
// rules...
}
`
Wrap your h4 with another element and do this
const StyledContainer = styled.div`
.wrapperClassName {
.title {
// rules...
}
}
`

ReactTestUtils.renderIntoDocument a component with children - children seem to "not render in DOM"

I render a component in my test like so:
component = TestUtils.renderIntoDocument(
<SlidingContainer {...props}>
<div style={divStyle}>foo</div>
<div style={divStyle}>foo</div>
<div style={divStyle}>foo</div>
</SlidingContainer>
);
$bodyElement = $(ReactDOM.findDOMNode(component.refs.body));
This is from SlidingContainer's render method
return (
<div className={rootClassName} >
<div className="sliding-container_arrow sliding-container_arrow--left"
onClick={this.slide.bind(this, 'left')}>
<div className={leftArrowClassName}></div>
</div>
<div className="sliding-container_body" ref="body">
{ this.props.children }
</div>
<div className="sliding-container_arrow sliding-container_arrow--right"
onClick={this.slide.bind(this, 'right')}>
<div className={rightArrowClassName}></div>
</div>
</div>
);
As you can see, $bodyElement is a DOM element that contains the children. It has a set width, whitespace: nowrap, overflow: hidden
This is updateScrollState method of `SlidingContainer'
updateScrollState($bodyElement) {
var bodyElement = $bodyElement[0];
var maxScrollLength = bodyElement.scrollWidth - bodyElement.offsetWidth;
let maxToTheRight = maxScrollLength === $bodyElement.scrollLeft()
let maxToTheLeft = $bodyElement.scrollLeft() === 0;
this.setState({
maxToTheRight: maxToTheRight,
maxToTheLeft: maxToTheLeft
});
}
This code works when I test it in the browser, but when running the karma/jasmine unit tests, bodyElement.scrollWidth and bodyElement.offsetWidth both equal 0. I searched around, and it seems this is an issue with an element being rendered in memory only and without real DOM, even though renderIntoDocument is supposed to render the element into a dom.
You need to mount it to DOM. Use window.body.appendChild($bodyElement)

How to replace one child react/html component with another based up on event using react?

I am new to react and I have a react component structure like:
<MainComponent>
<Button />
<Content />
</MainComponent>
Now when I click on the Button, I need to replace the existing div (say div1) of the Content component with another div (div2). Can you please tell me how to do it. Thank you.
Note: Till now, on click event I have been changing the data of the single div using state and prop. Now I got to replace the whole div with another one.
Like this.
render() {
var returnIt;
if (useDivOne) returnIt = (<div id='one'></div>);
else returnIt = (<div id='two'></div>);
return (returnItj);
}
If this is your structure:
<MainComponent>
<Button />
<Content />
</MainComponent>
and Content renders something like:
<div>
this is div 1
</div>
I would think you would need to pass a prop to Content that would tell you which div to render, then in Content's Render you manipulate the properties of Boolean logic to present a different component:
class Content extends Component {
render() {
return(
{
!this.props.RenderDiv2Bool &&
<div>
This is Div1 and it will be rendered
because RednerDiv2Bool is false.
</div>
}
{
this.props.renderDiv2Bool &&
<div>
This is Div2 and it will be rendered
because RednerDiv2Bool is true.
</div>
}
)
};
}
Not necessarily better but just another way to do it.

knockoutjs css binding value when boolean computed property is false/true

I am binding my currently selected item like this:
The selected item gets visible, but how can I give the unselected items an unselected/default background color? When I set a background color to the template class I do not see anymore the background color set from the selection via mouse click.
Is the problem maybe that I have 2 backgrounds set but none is removed?
<div data-bind="foreach: items">
<div class="cellContainer" >
<div class="template" data-bind="click: $parent.selectItem, css: { selected: isSelected }">
<div data-bind="text: number"></div>
</div>
</div>
</div>
.selected {
background-color: #0094ff;
}
Sounds like a cascade issue. Make sure your ".template" style is defined before your ".selected" style in your css file.
Make sure your selectItem method resets isSelected on all elements before setting it to true on the argument. A naive implementation could be:
var ViewModel = function() {
// Skipped the rest for brevity...
self.selectItem = function(item) {
// Deselect all items first
for (var i = 0; i < self.items().length; i++) {
self.items()[i].isSelected(false);
}
// Select the argument
item.isSelected(true);
};
};
See this jsfiddle for a demo.
However, it is often easier to keep a reference on the parent view model to the currently selected item, and change the css binding to something like:
css: { selected: $parent.TheOneSelectedItem().number() == number() }
See this fiddle for the alternate demo.
Something like this might work.
.template {
background-color: #fff;
}
.template.selected {
background-color: #0094ff;
}
It does not look like a knockout issue.

Expanding and collapsing multiple neighboring divs within the same parent element

I'm struggling with this bit of code, and I'm not sure if it's even possible. I have a list of divs within a single parent element, and I need to collapse and expand certain sets. Here's an example:
<div id="parent">
<div class="alway_show">ALWAYS SHOW ME</div>
<div class="collapse_me">COLLAPSIBLE</div>
<div class="collapse_me">COLLAPSIBLE</div>
<div class="collapse_me">COLLAPSIBLE</div>
<div class="alway_show">ALWAYS SHOW ME</div>
<div class="collapse_me">COLLAPSIBLE</div>
<div class="collapse_me">COLLAPSIBLE</div>
<div class="collapse_me">COLLAPSIBLE</div>
</div>
So, in the initial state, .collapse_me will be display:none. There will be a link in always show to expand ONLY the collapsed divs below that particular .always_show div. I know this would be ten million times easier if the collapsible divs were in their own div, but I don't have control over the code. I have to make it work as is using jquery. Possible?
$('div.always_show').nextAll().each(function() {
if($(this).is('.collapse_me')) {
$(this).toggle();
}
else {
//this will halt the each "loop", stopping before the next .always_show
return false;
}
});
Of course you should not use my initial selector 'div.always_show', but rather supply it the actual element, which will be the parent of the clicked link. For example:
$('#expand_anchor').parent().parent().nextAll()...
var fncdiv = function(){
var el = this;
do{
el = $(el).next();
if ($(el).hasClass("collapse_me") )
$(el).toggle();
else
break;
}while (true)
};
$('#parent div.alway_show').bind("click", fncdiv);
You shouldn't need to use jQuery. It only requires some clever CSS:
#parent
{
/* normal parent css here */
}
#parent div
{
display: block;
}
#parent.collapsed
{
display: block;
}
#parent.collapsed div
{
display: none;
}
Selectors are applied in order of specificity. Since '#parent.collapsed div' is more specific than '#parent div', it should override. Now, all you need to do, is set the parent div's class, and you're done. You can use javascript to add/remove the 'collapsed' class to the DIV at runtime to toggle expansion without any additional effort:
// Mootools:
$('parent').addEvent('click', function()
{
$('parent').toggleClass('collapsed')
});