child property from parent Promise - lit-element

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.

Related

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;
}

How to use React DnD with styled component?

When wrapping my styled component in connectDragSource I get the following error:
Uncaught Error: Only native element nodes can now be passed to React
DnD connectors.You can either wrap PaneItemText__StyledItem into a
<div>, or turn it into a drag source or a drop target itself.
The first suggestion from this message is to wrap my styled component in a <div>, but this will mess with my layout and would prefer not to do this.
I'm not sure what the second option is suggesting - would anybody be able to clarify?
Below is an rough example what I am doing:
import React, { Component } from 'react';
import styled from 'styled-components';
import { DragSource } from 'react-dnd';
const StyledComponent = syled.div`
...
`;
class MyComponent extends Component {
...
render() {
const { connectDragSource } = this.props;
return connectDragSource(<StyledComponent />)
}
}
const itemSource = {
beginDrag(props) {
/* code here */
},
endDrag(props) {
/* code here */
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
export default DragSource('foo', itemSource, collect(MyComponent);
You should use Styled Component's innerRef to get the underlying DOM node, then you can call your connectDragSource to it.
In your case, it should be like this:
class MyComponent extends Component {
...
render() {
const { connectDragSource } = this.props;
return (
<StyledComponent
innerRef={instance => connectDragSource(instance)}
/>
)
}
}
You can also look at my implementation of Knight component for the official chess tutorial as a reference.
It is also accessible through CodeSandbox.
If you are using multiple connectors you can do the following:
<MyStyledComponent
innerRef={instance => {
connectDragSource(instance);
connectDropTarget(instance);
}}
/>
Source: https://github.com/react-dnd/react-dnd/issues/347#issuecomment-221703726

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)
}
....
}

In react, I define a function but it's appear undefine

I defined a function, but it's undefined in the reference precess. I tried to add breakpoints, the results show that it is a function, but continues to perform an error again, you mean form the callback?
That's the part I quoted, prompt requestServer is not the function, but it has been defined in the code below, the reason why I don't know whether the callback
import React, {Component, PropTypes} from 'react';
import GLogin from './Login';
var serverMethon = require('../../server/requestServer');
export default class LoginContainer extends Component {
constructor(props) {
super(props);
this.state = {
}
}
getLogin = (value) => {
const {selectView} = this.props;
const requestServer = serverMethon.requestServer;
requestServer('login', value, function(t) {
const data = JSON.parse(t.text);
if (data.state != "successful") {
alert("Login fail!")
return;
}
selectView('SearchContainer');
})();
}
render() {
return (
<GLogin
getLogin={this.getLogin}
{...this.props}/>
)
}
}
Function definitions section
var superagent = require('superagent');
export const requestServer = (position, info, callback) => {
superagent.post(`http://localhost:3000/${position}`)
.send(info)
.end((error, doc)=>{
if(error){
throw error
}
callback(doc)
})
}
// proper way
class App extends React.Component{
constructor (props){
super(props);
this.sampleMethod = this.sampleMethod.bind(this);
}
sampleMethod(){
console.log('Sample method called');
}
render(){
return <button onClick={this.sampleMethod}>Click Me</buttom>
}
}
Your mistake is you called your method as this.getLogin. But you don't bind the method. So you can't use this method with this.getLogin. You can use getLogin method without binding. This time you called the method by getLogin but you get context inside getLogin method the context is not be your component. At the time your context is window so you get window object.
So first bind your method then use it.
// your mistake
getLogin = (value)=>{
// your logic
}
// proper way
getLogin(value){
// your logic
}

Resources