Can a text node be slotted? - javascript

Is it possible to assign a text node to a slot when using <template> and <slot>s?
Say I have a template that looks like
<template>
<span>
<slot name="mySlot"></slot>
</span>
</template>
I would like to be able to add only text to the slot, instead of having to add a <span> open and close tag each time I use the template. Is this possible? If not in pure HTML, in JavaScript?
It is also better to pass in the text content only so that no styling is applied on the way in. Currently I'm using an invalid tag <n> to avoid that issue.

Sure, you can with imperative slot assignment, but not yet in Safari.
You can not slot a text node into a named slot (declarative).
Mixing declarative and imperative slots is not possible.
::slotted(*) can not target text nodes.
https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Imperative-Shadow-DOM-Distribution-API.md
https://caniuse.com/mdn-api_shadowroot_slotassignment
<script>
customElements.define("slotted-textnodes", class extends HTMLElement {
constructor() {
super().attachShadow({
mode: 'open',
slotAssignment: 'manual' // imperative assign only
}).innerHTML = `<style>::slotted(*){color:red}</style>
Click me! <slot name="title"></slot> <slot>NONE</slot>!!!`;
}
connectedCallback() {
let nodes = [], node;
setTimeout(() => { // wait till lightDOM is parsed
const nodeIterator = document.createNodeIterator(
this, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => node.parentNode == this && (/\S/.test(node.data))
}
);
while ((node = nodeIterator.nextNode())) nodes.push(node);
this.onclick = (e) => {
this.shadowRoot.querySelector("slot:not([name])").assign(nodes[0]);
nodes.push(nodes.shift());
}
})
}
})
</script>
<slotted-textnodes>
Foo
<hr separator>Bar<hr separator>
<b>text INSIDE nodes ignored by iterator filter!</b>
Baz
<span slot="title">Can't mix Declarative and Imperative slots!!!</span>
</slotted-textnodes>

Related

Using slots in WebComponents without using shadow DOM

I'm trying to build a WebComponent without using ShadowDOM - so far it mostly just worked, but now I want to build a component that wraps other components like you would do with Angular's #ViewChild / #ViewChildren. (the library I'm using here to render is uhtml similar to lit-html)
export class Dropdown extends HTMLElement {
private open: boolean = false;
static observedAttributes = ["open"]
constructor() {
super();
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
switch (name) {
case "open":
this.open = Boolean(newValue);
break;
}
this.display()
}
connectedCallback() {
this.display()
}
display = () => {
render(this, html`
<div>
<slot name="item">
</slot>
</div>
`)
}
static register = () => customElements.define("my-dropdown", Dropdown)
}
If I now use this component
Dropdown.register();
render(document.body, html`
<my-dropdown open="true">
<strong slot="item">test</strong>
</my-dropdown>
`)
I'm expecting to see
<my-dropdown>
<div>
<strong>test</test>
</div>
</my-dropdown>
but the slot part is not working.
If I switch to ShadowDOM, it just works, but now I have to deal with ShadowDOM's sandbox with regards to styling which I'd rather not do.
constructor() {
super();
this.attachShadow({mode: "open"})
}
display = () => {
render(this.shadowRoot as Node, html`
Is it possible to make slots work without shadowDOM?
If not, is there different way to grab the content defined inside the component and use it inside display?
<my-component>
<div>some content here</div>
</my-component>
should render as
<my-component>
<header>
random header
</header>
<section>
<!-- my custom content -->
<div>some content here</div>
</section>
</my-component>
Any suggestions?
No, <slot> are part of the shadowDOM API
You can fake it, but since there is no shadowDOM you would have to store that content someplace else.
Could be a <template> you read and parse your (light)DOM content into.
That is a sh*load of DOM mutations.
Might be easier to just learn to style shadowDOM with:
CSS properties
inheritable styles
::part
constructable stylesheets

Add eventListener or timeout to vanilla JS component after rendering?

I'm currently rendering a component in vanilla JS by calling a method attached to a class where the component is defined, and returned.
class ToastNotification {
constructor(paramsObj) {
this.notificationType = paramsObj.notificationType || "info";
this.notificationAction = paramsObj.notificationAction || "none";
this.title = paramsObj.title || "Something happened";
this.message = paramsObj.message || `Here's some more specific information on what happened`;
}
getHtml() {
return `
<div
class="toastNotification notification_${this.notificationType} ${this.enableAnimations()}"
>
<div class="notification_icon">
<ion-icon name="${this.icon}"></ion-icon>
</div>
<div class="notification_text">
<p class="text_title">${this.title}</p>
<p class="text_message">${this.message}</p>
<p class="text_time">${this.timeSinceNotification()}</p>
</div>
<div class="notification_close">
<ion-icon name="close" onclick="this.closest('.toastNotification').remove()"></ion-icon>
</div>
</div>
`;
}
}
But how can I add an event listene or set a timeout to take an action on the component, after I have called the getHTML() method to render the element?
As I would like to be able to set a timeout for the rendered element to dissapear after X amount of time and add an event listener to enable different actions when clicked on. But I am open to alternatives if they can get the same job done.
The best result I've come up with so far is using the MutationObserver.
const observer = new MutationObserver((change) => {
change[0].addedNodes.forEach( newNode => {
if (newNode.tagName == 'DIV' && newNode.classList.contains('toastNotification')) {
// Add listeners or do an action
}
});
});
And then attaching the observer to the HTML document, or the body
observer.observe(document.querySelector('html'), {attributes: false, childList: true, subtree: true,});

javascript created template element not working [duplicate]

This question already has answers here:
Cannot get content from template
(2 answers)
Closed 3 years ago.
If I write a template node into the HTML by hand I can use it in my custom element just fine. If I create a template node and append it to the HTML using javascript when I try to use it, it's empty...
in the example below I make template-a the regular HTML way and make template-b to be the same shape using javascript. I define a very simple custom element that uses both templates. only template-a is visible.
const sandbox = document.getElementById('sandbox')
const slot = document.createElement('slot')
slot.setAttribute('name', 'b')
slot.append('slot content goes here')
const em = document.createElement('em')
em.append(slot, '?')
const createdTemplate = document.createElement('template')
createdTemplate.setAttribute('id', 'template-b')
createdTemplate.append(em)
sandbox.append(createdTemplate)
customElements.define('test-element', class extends HTMLElement {
constructor () {
super()
this.attachShadow({ mode: 'open' }).append(
...['template-a','template-b']
.map(id =>
document.getElementById(id).content.cloneNode(true)
)
)
}
})
<div id="sandbox">
<template id="template-a">
<strong><slot name="a">slot content goes here</slot>!</strong>
</template>
<test-element>
<span slot="a">some a slot content</span>
<span slot="b">some b slot content</span>
</test-element>
</div>
Some notes on your code:
this.shadowRoot
this.shadow = this.attachShadow({ mode: 'open' })
can become
this.attachShadow({ mode: 'open' })
this creates/sets this.shadowRoot for free
appendChild vs. append
Note that .appendChild(el) takes one element
and .append() takes an Array
Only difference is appendChild() returns a reference to the inserted element,
and append() returns nothing
So you can write:
em.appendChild(slot)
em.appendChild(document.createTextNode('?'))
as
em.append(slot, document.createTextNode('?'))
If you have Nodes in an Array:
let myElements = [slot, document.createTextNode('?')];
you can use the ES6 spread opperator:
em.append(...myElements)
This means you can write:
this.shadow.appendChild(document.getElementById('template-a').content.cloneNode(true))
this.shadow.appendChild(document.getElementById('template-b').content.cloneNode(true))
as:
this.shadowRoot
.append(
...['a','b']
.map(templ => document.getElementById(`template-${templ}`).content.cloneNode(true))
)
template nodes have a special content attribute which holds their children. (which I kind of knew but thought it was a little more magical than it is). If this line:
createdTemplate.append(em)
is changed to
createdTemplate.content.append(em)
then it all works
const sandbox = document.getElementById('sandbox')
const slot = document.createElement('slot')
slot.setAttribute('name', 'b')
slot.append('slot content goes here')
const em = document.createElement('em')
em.append(slot, '?')
const createdTemplate = document.createElement('template')
createdTemplate.setAttribute('id', 'template-b')
createdTemplate.content.append(em)
sandbox.append(createdTemplate)
customElements.define('test-element', class extends HTMLElement {
constructor () {
super()
this.attachShadow({ mode: 'open' }).append(
...['template-a','template-b']
.map(id =>
document.getElementById(id).content.cloneNode(true)
)
)
}
})
<div id="sandbox">
<template id="template-a">
<strong><slot name="a">slot content goes here</slot>!</strong>
</template>
<test-element>
<span slot="a">some a slot content</span>
<span slot="b">some b slot content</span>
</test-element>
</div>

JavaScript recursive array to select html elements

I'm currently trying to make a recursive function that takes html elements as an array so I can take html elements like the querySelector function
The reason i'm doing this is because I can't use getElementsByTagName() or querySelector()
Here is my code:
function flatten(items)
{
const flat = [];
items.forEach(item => {
if (Array.isArray(item)) {
flat.push(...flatten(item));
}
else {
flat.push(item);
}
});
return flat;
}
var button = flatten(footer).flatten(div);
count = 0;
button.onclick = function() {
count += 1;
button.innerHTML = count;
};
I get the following error: ReferenceError: footer is not defined
Thanks
Here is my HTML code:
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
Edit:
footer is defined in my HTML, I want to select footer in my function
Also, I can't add class or id to my html, I can't edit it
If, for the sake of practice (or a lost bet), you'd want to write your own querySelectorAll, you could write a recursive function that walks the DOM tree... The only thing you rely on is an entrance to the DOM: window.document.
Note that this will never be able to compete with the performance of your browser's default query implementations. We're just doing it to show we can.
Step 1: recursively walking the document (depth-first)
const walk = (el) => {
console.log(el.nodeName);
Array.from(el.children).forEach(walk);
};
walk(document);
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
As you can see, this function loops over each element in the document and its children.
Step 2: Adding the filter logic
If you want it to actually find and return elements, you'll have to pass some sort of filtering logic. querySelectorAll works with string inputs, which you could try to recreate... Since we're redoing this for fun, our select will work with functions of HTMLElement -> bool.
const selectIn = (pred, el, result = []) => {
if (pred(el)) result.push(el);
Array.from(el.children)
.filter(e => e)
.map(el2 => selectIn(pred, el2, result));
return result;
}
// EXAMPLE APP
// Define some selectors
const withClass = className => el =>
el && el.classList && el.classList.contains(className);
const withTag = tagName => el =>
el && el.nodeName === tagName.toUpperCase();
// Select some elements
const footer = selectIn(withTag("footer"), document)[0];
const container = selectIn(withClass("container"), document)[0];
const divsInFooter = selectIn(withTag("div"), footer);
// Log the results
console.log(`
footer:
${footer.outerHTML}
container:
${container.outerHTML}
divsInFooter:
${divsInFooter.map(d => d.outerHTML)}
`);
<div class="wrapper"><div class="container"><footer><div></div></footer></div></div>

Using Javascript loop to create multiple HTML elements

I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>

Categories