I'm working on new web-components and ran into some issues concerning slots.
I have an outer container-component with a "main-slot" inside, in which multiple elements should be inserted (into the same slot). However, it is only possible to add one element per named slot.
My question: is there a way to add multiple elements to one named slot? Like shown here:
<own-container>
<own-element slot="main"></own-element>
<own-element slot="main"></own-element>
<own-element slot="main"></own-element>
<own-element slot="main"></own-element>
</own-container>
There is also imperative <slot>
super().attachShadow({
mode: 'open',
slotAssignment: 'manual' // imperative assign only
})
But! you get Named slots OR Imperative slots, on one Web Component
To mimic named <slot>, assigning content to the same named <slot>
you probably need the Mutation Observer API
addendum
You can have multiple elements per slot:
<component-with-slots>
<H1 slot="title">Web Components are great!</H1>
<h2 slot="title">A bit hard to master</h2>
<b slot="title">But Great!</b>
</component-with-slots>
<script>
customElements.define('component-with-slots', class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:'open'})
.innerHTML="<slot name='title'></slot>";
}
});
</script>
Nope. It is not possible for named slot. The trick is to have a wrapper div element to wrap your lightDOM.
<own-container>
<div slot="main">
<own-element></own-element>
<own-element></own-element>
<own-element></own-element>
<own-element></own-element>
</div>
</own-container>
If the presence of additional div causes styling problem, then you can use new contents type of display box.
div {
display: contents;
}
The display: contents causes an element's children to appear as if they were direct children of the element's parent, ignoring the element itself. However, note that it can cause accessibility issues.
Related
In the below code I am trying to assign <span slot='test-slot'>b</span> to <slot name='test-slot'>a</slot> but the assignment does not work. If I bring <span slot='test-slot'>b</span> outside of its parent <div> container the assignment does take place as expected.
Why is this? Is there anyway you can assign from nested elements with the slot element? If not, any alternatives? This is obviously a reduced test case but in my real web component, it is much more intuitive for a user to add an element with the slot tag within other containers.
<test-element>
<div>
<span slot='test-slot'>b</span>
</div>
</test-element>
<template id='template-test-element'>
<slot name='test-slot'>non slotted content</slot>
</template>
<script>
class TestElement extends HTMLElement {
constructor() {
let template = document.getElementById("template-test-element")
.content.cloneNode(true);
// Initialise shadow root and attach table template
super() // sets AND return 'this' scope
.attachShadow({mode:"open"}) // sets AND returns shadowRoot
.append(template);
}
}
customElements.define('test-element', TestElement);
</script>
Named slots only accept top-level children that have a matching slot-attribute.
See this (old) Polymer explainer or this more recent article.
Edit: Not sure where this is coming from though, the spec fails to mention this requirement: https://html.spec.whatwg.org/multipage/dom.html#attr-slot
It also is neither mentioned here nor here.
I have multiple elements which each contain the same class name appended with a number, created dynamically depending on where in the html structure they appear, e.g. myclass-1, myclass-2, myclass-4. There could potentially (though unlikely) be unlimited class numbers.
I would like - in vanilla Javascript (no jQuery) - to add a single click function so that if the element contains any class-n except class-1 to toggle another class. I can do it individually, like this:
document.getElementById("myId").addEventListener("click", toggleClass);
function toggleClass(event){
if(event.target.classList.contains("myclass-2")){
event.target.classList.toggle("another-class");
}
}
I could repeat this code multiple times, changing "myclass-2" to "myclass-3" and so on. Is there a way to do this in one go, excluding myclass-1? N.B. I don't think it's possible to change "myclass-1" to another class name automatically on page load (or not to my limited Javascript knowledge, anyway).
If I was including "myclass-1" I could do something like this:
let existingClass = document.getElementsByClassName("*[class^=\"myclass-\"]")
But that isn't suitable here because of my need to exclude "myclass-1".
Would it be a push function? I'm very much a JS beginniner and only know the very basics of the push function, with a 'console.log' output, which doesn't help me here.
Thank you.
You can use event delegation - add a single click listener to the window. On click, if the target is a myclass and is not a myclass-1, toggle the target.
Use :not(.myclass-1) to exclude myclass-1.
window.addEventListener('click', (e) => {
if (e.target.matches('[class^="myclass"]:not(.myclass-1')) {
e.target.classList.toggle('another-class');
}
});
.another-class {
background-color: yellow;
}
<div class="myclass-1">one</div>
<div class="myclass-2">two</div>
Forgive my ignorance, I'll try to be as clear as possible. I'm trying to modify an existing JS function that adds a class to a parent element. I would like to modify it to add the same class to a sibling of the parent, in this case the aside '.footnote-right-col'.
I have tried a good number of ways but this is beyond me. I'm not sure if I need to create a new function and call it separately, or if I can simply add a new variable to this function. I presume the way I would target the parent is via getParent.nextSibling
As is JS
open: function (el) {
this.footnote.el = getParent(el, '.footnote-container')
var popover = this.footnote.popover()
this.footnote.el.classList.add('is-open')
this.sizeFootnote()
popover.classList.remove('is-hidden')
this.positionFootnote()
popover.classList.add('is-visible')
window.addEventListener("resize", self.resize.bind(self))
},
Rendered HTML
<div class="footnote-container open-down">
<button class="footnote-button" title="view footnote #1">...</button>
<aside class="footnote-popover is-hidden">...</aside>
</div>
<aside class="footnote-right-col is-hidden">...</aside>
this.footnote.el.nextElementSibling Should get you .footnote-right-col element if I am understanding how you set things. If this.footnote.el is the <div class="footnote-container open-down"> element then my answer works. Once you have that div you can just call the .nextElementSibling
EDIT:
ok so cool if this.footnote.el is the button then simply do this:
this.footnote.el.parentElement.nextElementSibling and you should have it!
So to add the class:
this.footnote.el.parentElement.nextElementSibling.classList.add('is-open')
I have a jQuery plugin (which I don't want to modify) that is dynamically creating a div. Aside from that, I have a webcomponent scrollable-div, which is a customized built-in extended from HTMLDivElement. As I have no control over how that div is created by the jQuery plugin, I need to upgrade it after creation and after it has already been added to the DOM.
class myDiv extends HTMLDivElement {
constructor(...args) {
const self = super(...args);
self.addEventListener('click', (e) => {
e.target.textContent = 'clicked'
})
return self;
}
}
customElements.define('my-div', myDiv, { extends: 'div' });
document.addEventListener('DOMContentLoaded', () => {
// this is where I'm trying to turn the div#upgradeMe into a my-div
upgradeMe.setAttribute('is', 'my-div');
});
<div id="upgradeMe">Click me</div>
Simply adding the is="my-div" attribute obviously does not do the trick, the div simply stays a regular HTMLDivElement. How can I programmatically upgrade a native element that is already in the DOM to a customized built-in web component?
It's not possible because the element is already created as a standard <div> element and not identified when parsed as upgradable (extendable) due to the lack of the is attribute.
If the custom element is already defined, the only possible workaround is to replace the existing by a clone (as suggested in the comments by #barbsan).
The short way:
create a <template> element
copy the div's outerHTML into its innerHTML property
replace the orginal element with the template's content with replaceChild()
class myDiv extends HTMLDivElement {
constructor(...args) {
const self = super(...args);
self.addEventListener('click', (e) => {
e.target.textContent = 'clicked'
})
return self;
}
}
customElements.define('my-div', myDiv, { extends: 'div' });
document.addEventListener('DOMContentLoaded', () => {
// this is where I'm trying to turn the div#upgradeMe into a my-div
upgradeMe.setAttribute('is', 'my-div');
var t = document.createElement( 'template' )
t.innerHTML = upgradeMe.outerHTML
upgradeMe.parentElement.replaceChild( t.content, upgradeMe )
});
<div id="upgradeMe">Click me</div>
Précisions
When an element is parsed, an is value is affected according to the DOM spec:
Elements have an associated namespace, namespace prefix, local name, custom element state, custom element definition, is value. When an element is created, all of these values are initialized.
Only elements with a valid is attribute are identified as customizable:
An element’s custom element state is one of "undefined", "failed", "uncustomized", or "custom". An element whose custom element state is "uncustomized" or "custom" is said to be defined. An element whose custom element state is "custom" is said to be custom.
Therefore if the element has no is attribute at parse time, it will not be customizable. That's why you cannot add the is attribute afterward.
Also in the HTML specs:
After a custom element is created, changing the value of the is attribute does not change the element's behavior, as it is saved on the element as its is value.
The is attribute is used only at element creation (at parse time) to initialize the is value and has no effect if changed when the element is already created. In that sense is value is read-only.
If you want to support all modern browser's you can't customize built in components, Apple said they will never support is="" https://github.com/w3c/webcomponents/issues/509#issuecomment-222860736
Let's say:
<div>
pre text
<div class="remove-just-this">
<p>child foo</p>
<p>child bar</p>
nested text
</div>
post text
</div>
to this:
<div>
pre text
<p>child foo</p>
<p>child bar</p>
nested text
post text
</div>
I've been figuring out using Mootools, jQuery and even (raw) JavaScript, but couldn't get the idea how to do this.
Using jQuery you can do this:
var cnt = $(".remove-just-this").contents();
$(".remove-just-this").replaceWith(cnt);
Quick links to the documentation:
contents( ) : jQuery
replaceWith( content : [String | Element | jQuery] ) : jQuery
The library-independent method is to insert all child nodes of the element to be removed before itself (which implicitly removes them from their old position), before you remove it:
while (nodeToBeRemoved.firstChild)
{
nodeToBeRemoved.parentNode.insertBefore(nodeToBeRemoved.firstChild,
nodeToBeRemoved);
}
nodeToBeRemoved.parentNode.removeChild(nodeToBeRemoved);
This will move all child nodes to the correct place in the right order.
You should make sure to do this with the DOM, not innerHTML (and if using the jQuery solution provided by jk, make sure that it moves the DOM nodes rather than using innerHTML internally), in order to preserve things like event handlers.
My answer is a lot like insin's, but will perform better for large structures (appending each node separately can be taxing on redraws where CSS has to be reapplied for each appendChild; with a DocumentFragment, this only occurs once as it is not made visible until after its children are all appended and it is added to the document).
var fragment = document.createDocumentFragment();
while(element.firstChild) {
fragment.appendChild(element.firstChild);
}
element.parentNode.replaceChild(fragment, element);
$('.remove-just-this > *').unwrap()
More elegant way is
$('.remove-just-this').contents().unwrap();
Use modern JS!
const node = document.getElementsByClassName('.remove-just-this')[0];
node.replaceWith(...node.childNodes); // or node.children, if you don't want textNodes
oldNode.replaceWith(newNode) is valid ES5
...array is the spread operator, passing each array element as a parameter
Replace div with its contents:
const wrapper = document.querySelector('.remove-just-this');
wrapper.outerHTML = wrapper.innerHTML;
<div>
pre text
<div class="remove-just-this">
<p>child foo</p>
<p>child bar</p>
nested text
</div>
post text
</div>
Whichever library you are using you have to clone the inner div before removing the outer div from the DOM. Then you have to add the cloned inner div to the place in the DOM where the outer div was. So the steps are:
Save a reference to the outer div's parent in a variable
Copy the inner div to another variable. This can be done in a quick and dirty way by saving the innerHTML of the inner div to a variable or you can copy the inner tree recursively node by node.
Call removeChild on the outer div's parent with the outer div as the argument.
Insert the copied inner content to the outer div's parent in the correct position.
Some libraries will do some or all of this for you but something like the above will be going on under the hood.
And, since you tried in mootools as well, here's the solution in mootools.
var children = $('remove-just-this').getChildren();
children.replaces($('remove-just-this');
Note that's totally untested, but I have worked with mootools before and it should work.
http://mootools.net/docs/Element/Element#Element:getChildren
http://mootools.net/docs/Element/Element#Element:replaces
I was looking for the best answer performance-wise while working on an important DOM.
eyelidlessness's answer was pointing out that using javascript the performances would be best.
I've made the following execution time tests on 5,000 lines and 400,000 characters with a complexe DOM composition inside the section to remove. I'm using an ID instead of a class for convenient reason when using javascript.
Using $.unwrap()
$('#remove-just-this').contents().unwrap();
201.237ms
Using $.replaceWith()
var cnt = $("#remove-just-this").contents();
$("#remove-just-this").replaceWith(cnt);
156.983ms
Using DocumentFragment in javascript
var element = document.getElementById('remove-just-this');
var fragment = document.createDocumentFragment();
while(element.firstChild) {
fragment.appendChild(element.firstChild);
}
element.parentNode.replaceChild(fragment, element);
147.211ms
Conclusion
Performance-wise, even on a relatively big DOM structure, the difference between using jQuery and javascript is not huge. Surprisingly $.unwrap() is most costly than $.replaceWith().
The tests have been done with jQuery 1.12.4.
if you'd like to do this same thing in pyjamas, here's how it's done. it works great (thank you to eyelidness). i've been able to make a proper rich text editor which properly does styles without messing up, thanks to this.
def remove_node(doc, element):
""" removes a specific node, adding its children in its place
"""
fragment = doc.createDocumentFragment()
while element.firstChild:
fragment.appendChild(element.firstChild)
parent = element.parentNode
parent.insertBefore(fragment, element)
parent.removeChild(element)
If you are dealing with multiple rows, as it was in my use case you are probably better off with something along these lines:
$(".card_row").each(function(){
var cnt = $(this).contents();
$(this).replaceWith(cnt);
});
The solution with replaceWith only works when there is one matching element.
When there are more matching elements use this:
$(".remove-just-this").contents().unwrap();