Do something when slot does not have child nodes in lit-html - lit-element

I'd like to display a text when no content is on the slot.
class List extends LitElement {
public render() {
return slot.length === 0
? html`No content is available`
: html`<slot></slot>`;
}
}

I think this may help :
render() {
return html` <slot id="slot">No content is available</slot>
`;}
firstUpdated(){
const slot = this.shadowRoot.querySelector("#slot");
this.slt = slot.assignedNodes();
if (this.slt.length===0){
console.log('No content is available')
} else {
console.log('Content available', this.slt)
}
}
You can not slot's assigned nodes unless you render slot element. That's why first you need to render it. After there are a lot way to hide it. Another way is to dedect it at it's parents and pass the nr of slot element as property.
Demo

Related

child property from parent Promise

I want to create a navigation component for my project. The shell fetches a json with chapter info, these are passed to nav-element, which recursively calls itself to render the navigation tree.
shell.js
import { LitElement, html, css } from 'lit-element';
import {until} from 'lit-html/directives/until.js';
import './nav-element.js';
export class Shell extends LitElement {
static get properties() {
return {
configjson : { type: Array }
};
}
constructor() {
super();
this.configjson = fetch('./src/convertjson_test.json').then(res => res.json());
}
render() {
return html`
<main>
some content
<nav-element .chapters=${until(this.configjson, [])} root></nav-element>
</main>
`;
}
}
customElements.define('shell', Shell);
nav-element.js
import { LitElement, html, css } from 'lit-element';
import {until} from 'lit-html/directives/until.js';
import {repeat} from 'lit-html/directives/repeat.js';
export class NavElement extends LitElement {
static get properties() {
return {
chapters: {type: Array},
root: {type: Boolean} //to mark the root node
};
}
static get styles() {
return css`
.navheader {
display: none;
}
.navheader[active] {
display: block;
}
`;
}
render() {
return html`
<div class="navHeader" ?active="${this.root}">header</div>
${until(repeat(this.chapters, (chapter) => chapter.pos, (chapter) => html`<div>${chapter.n}<nav-element .chapters=${chapter.c}></nav-element></div>`))}
`;
}
}
customElements.define('nav-element', NavElement);
The problem is, that the configjson Promise is passed as property and not yet resolved by the time the nav-element is called, so i get the error:
Uncaught (in promise) TypeError: this.chapters is undefined
Searched all lit-element and lit-html documentation, the until directive resolved the issue in the shell, but not in the nav-element.
The same coding pattern worked fine in Polymer 2 (&3, although with ajax instead of fetch). Does anyone know how to solve this using lit-element only?
There is a time frame between the construction of NavElement and the assignment of the chapters property where chapters is undefined. It might be safe to initialize chapters in the component itself rather than in Shell's until directive, or at least provide a fallback value in the template:
export class NavElement extends LitElement {
static get properties() {
return {
chapters: {type: Array},
// ...
};
}
constructor() {
super();
this.chapters = [];
}
// or
render() {
return html`
...
${repeat(this.chapters || [], chapter => ...)}
`;
}
}
Also, you've (correctly) declared the chapters property as an Array but you're wrapping the repeat directive in an until (as if it was a Promise I guess?). Here there are two things going on:
the repeat() call returns a DirectiveFn and not a Promise like until expects. If chapters was a Promise, the correct way to combine until and repeat would have been:
until(
this.chaptersPromise.then(chapters => repeat(chapters, chapter => html`...`)),
html`Loading...`,
)
but...
chapters is not a Promise: the until call in the parent component resolves it and passes the result.
As for the Shell component: until used in this way should work, however its intended use is to
Render placeholder content until the final content is available
[from lit-html docs]
To make the most of it, use it to temporarily render a loading template instead of <nav-element>:
render() {
return html`
${until(
this.configJson.then(chapters => html`<nav-element .chapters=${chapters}></nav-element>`),
html`Loading chapters`,
)}
`;
}
Also, not a big deal, but here the configJson property is declared as an Array but is actually a Promise.

How do you access a lit-element render root after overriding createRenderRoot?

I've been playing around with lit-element, and I want to grab my custom element to run a getElementById. The only examples I can find use the shadow root (since that's the recommended way to use lit-element). How do you get access to your custom element to run a query on just your element?
import { LitElement, html }
from 'https://unpkg.com/lit-element/lit-element.js?module';
class RenderRootTest extends LitElement {
constructor() {
super();
}
render () {
const renderRoot = this.shadowRoot; //Won't work, because I'm overriding the shadowroot
return html`
<div>Rendered</div>
${renderRoot ?
html`<div>Render root found</div>` :
html``
}
`;
}
createRenderRoot() {
return this;
}
}
customElements.define('render-root-test', RenderRootTest);
I found the answer myself after enough tinkering. You can either use this.renderRoot or just this. However, note that certain methods such as .getElementById don't seem to exist. If anyone has any additional details on this topic, I would appreciate it.
ex.
import { LitElement, html }
from 'https://unpkg.com/lit-element/lit-element.js?module';
class RenderRootTest extends LitElement {
constructor() {
super();
}
render () {
const renderRoot = this.renderRoot;
return html`
<div>Rendered</div>
${renderRoot ?
html`<div>Render root found</div>` :
html``
}
`;
}
createRenderRoot() {
return this;
}
}
customElements.define('render-root-test', RenderRootTest);
Just reference this which is the instance of the custom element. The shadowRoot is created and returned by LitElement's createRenderRoot() so if you don't create one and instead of this.shadowRoot return this--which is the node itself--that is what the content is rendered into, there is no shadowRoot.

Render after getting props

I can't see a proper reason for a LitElement WebComponent to be rendered before having its props/attributes available. If the template needs a prop, you are forced to render a second time the component, after the first useless one. Worse than that, if your component looks like:
class MyComp extends LitElement
static get properties() {
return {
myBigProp: {
type: Object
}
render() {
return html`<p> ${this.myBigProp.nestedProp}</p>`
}
you get an error for accessing nestedProp when myBigProp is undefined. Is there a clean way to avoid rendering twice this component?
If you already are able to set the properties you can simply define them in the constructor. Then it should be accessible at the first render.
constructor() {
super();
this.myBigProp = {nestedProp:'nested value'}
}
However if you have to wait for them I usually make it clear that the component is loading visually by using an "initialized" property then handling it in the render function.
render() {
if (!this.initialized) {
return html`<div class='loading-reserved-zone'></div>`;
} else {
return html`<p>${this.myBigProp.nestedProp}</p>`
}
}
Otherwise, "Optional Chaining Operator" are coming up to javascript in august 2020 which could handle your second scenario or you could technically use this babel plugin if you want to use it earlier.
There is a lifecycle callback shouldUpdate. Return false until you want to render the component.
shouldUpdate(changedProps) {
return this.myBigProp != null;
}

passing function as a props

I'm not able to understand how to call a function from parent component to a child component in React. It just calls the functions from TestEvent and renders the old data on form. Control not going to ManageComment's component functions.
class TestEvent extends Component{
constructor(props){
super(props);
this.state={
editing:false,
comment:this.props.comment
};
this.commentRef=React.createRef(); // to create a ref to the DOM element
}
edit(){
this.setState({editing:true}) ;
}
save(){
this.setState({editing:false});
this.props.updatecomment.bind(this.commentRef.current.value,this.props.index);
}
remove(){
this.props.deletecomment.bind(this.props.index);
}
renderNormal(){
return(
<div className="commentContainer">
<div className="commentText">{this.state.comment}</div>
<div className="commentBtn">
<button className="b1" onClick={this.edit.bind(this)} >Edit</button>
<button className="b2" onClick={this.remove.bind(this) } >Remove</button>
</div>
</div>
);}
renderForm(){
return(
<div className="commentContainer">
<textarea ref={this.commentRef} defaultValue={this.state.comment}></textarea>
<div className="commentBtn">
<button className="b3" onClick={this.save.bind(this)} >Save</button>
</div>
</div>
);
}
render(){
if(this.state.editing){
return this.renderForm();
}else{
return this.renderNormal();
}
}
}
// Need to manage comments from separate component here
class ManageComment extends Component{
constructor(props){
super(props);
this.state= {
comments:[
'First comment',
'Second Comment',
'Third comment'
]}
}
// add functions to remove,edit comments from child
// passing functions as a props
updateComment(newComment,i){
var arr=this.state.comments;
arr[i]=newComment;
this.setState({comments:arr});
}
deleteComment(i){
var arr=this.state.comments;
arr.splice(i,1);
this.setState({comments:arr});
}
render(){
return(
<div className="manageComment">
{
this.state.comments.map(function(text,i){
return (
<TestEvent
key={i}
index={i}
comment={text}
updatecomment={this.updateComment.bind(text,i)}
deletecomment={this.deleteComment.bind(i)}> </TestEvent>);
},this)
}
</div>
);
}
}
.
I've created a form to enter a comment in textarea,and giving options to edit/save/remove a comment.
Details of my code :
I've created 'TestEvent' component,wherein I've 3 options,Save,Remove and Edit to operate on Comment-Text.
Then I've 'ManageComment' component, where I'm rendering 'TestEvent' with some props as well as I'm passing functions with props called updatecomment and deletecomment.
Finally,I'm rendering 'ManageComment' component.
so basically here I've parent and child component and I'm trying to invoke function of 'ManageComment' component from 'TestEvent'.
The problem is that you don't call the function but just bind() something to your functions. For example in your function you do following:
save = () => {
this.setState({editing:false});
this.props.updatecomment.bind(this.commentRef.current.value,this.props.index);
}
The problem is that this.props.updatecomment.bind(this.commentRef.current.value,this.props.index); does not execute your function but just binds the context. I would suggest you have another look at bind. What you want to do is to call your function instead of binding something to it e.g. this.props.updatecomment(this.commentRef.current.value,this.props.index);.
Why exactly do you need a commentRef? You could just access the value of your textarea through the event which gets called when you save it. If you call the event you can find the data in event.target. Check in it where the value for your text area is.
save = (event) {
this.setState({editing:false});
const commentValue = event.target
this.props.updatecomment(commentValue, this.props.index)
}
Also a suggestion from my side to make your code more readable I would suggest you use arrow functions if you want to bind the context of your component
class ManageComment extends Component{
constructor(props){
super(props);
this.state= {
comments:[
'First comment',
'Second Comment',
'Third comment'
]}
}
// add functions to remove,edit comments from child
// passing functions as a props
updateComment = (newComment,i) =>{
var arr=this.state.comments;
arr[i]=newComment;
this.setState({comments:arr});
}
deleteComment = (i) => {
var arr=this.state.comments;
arr.splice(i,1);
this.setState({comments:arr});
}
render(){
return(
<div className="manageComment">
{
this.state.comments.map(function(text,i){
return (
<TestEvent
key={i}
index={i}
comment={text}
updatecomment={this.updateComment}
deletecomment={this.deleteComment}> </TestEvent>);
},this)
}
</div>
);
}
}
Another suggestion is that you use let and const instead of var. There is a lot of literature out there on why it makes sense to no longer use var -> literature. As a rule of thumb you can just always use const and when the compiler complains switch to let;
You have already passed updateComment() and deleteComment() as props named updatecomment and deletecomment for TestEvent component. But, those functions should be bound like so:
updatecomment={this.updateComment.bind(this)}
deletecomment={this.deleteComment.bind(this)}
You don't need to bind those functions again. It's only a matter of using it like so:
class TestEvent extends Component {
....
save() {
this.setState({editing:false})
this.props.updatecomment(this.commentRef.current.value,this.props.index))
}
remove() {
this.props.deletecomment(this.props.index)
}
....
}

Geb: List of Page contents

I have a Page class as
class SignUpPage extends Page {
static url = "signup"
static at = { waitFor { title.startsWith("Join") } }
static content = {
firstNameField { $("input", name:"firstName") }
lastNameField { $("input", name:"lastName") }
emailField { $("input", name:"email") }
passwordField { $("input", name:"password") }
}
}
I want to add a populateFields method to this class. This will allow me to call this method to populate the text fields from my test cases. This method has one argument passed in - a Map that allows me to override certain field values as necessary from my test cases.
The problem is that I don't know how I can iterate over the 'content' of the page. To make this clearer look at the code below:
class SignUpPage extends Page {
static url = "signup"
// .. as defined above ..
def populateFields(customValues = [:]) {
// I want to iterate of the textFields here
// Something like...
textFields = this.metaclass.methods.findAll {
it.name.endsWith("Field")
}
textFields.each {
// populate with data
}
}
}
This doesn't work.
How do I get the content of the closure 'content'?
I think that there is a much easier way of implementing it and you don't need to iterate over contents of your page object. Given the keys in your map are name attributes of the inputs you want to modify, you can do the following:
def populateFields(customValues = [:]) {
def form = $('form') //can be any element that is enclosing all of your inputs
customValues.each { key, value ->
form[key] = value
}
}
Have a look at the section on form control shortcuts in the manual to understand how it works.
If content becomes too complicated to use the available tools you could always create a list of the page contents in your content.
static content = {
username { module $(... }
contactTitle { $(... }
contactGivenName { $(... }
contactFamilyName { moduleList $(... }
pageFields {
[
username,
contactTitle,
contactGivenName,
contactFamilyName,
]
}
}
def populateFields(valueList) {
pageFields.each {
it.value(somevaluefromList)
}
}

Resources