I am using the react-event-timeline lib (here: https://www.npmjs.com/package/react-event-timeline) to create a timeline for one of my pages.
I create a list of events called: events and pass it to the timeline. The timeline consists of two parts the first is the initial event, when the page is created there is a timeline event at the top of each timeline to catch that. The second part is dynamically generated based on the list of events provided by the state of the page.
React throws this timeline error and based on debugging I think the issue is with the structure of the html below where The && checks are:
The error I get is:
React.cloneElement(...): The argument must be a React element, but you passed null.
The above error occurred in the component:
in Timeline (created by TimelineComponent)
<Timeline className={styles.timeline}>
{(data) && (<TimelineEvent
createdAt={Moment(data.creationDateTime).fromNow()}
icon={<ActionIcon variant={'create'} fontSize={18} />}
iconStyle={this.bubbleStyles('create').icon}
bubbleStyle={this.bubbleStyles('create').bubble}
subtitle={this.emptyContainer()}
contentStyle={contStyle}
title={this.creationTitle(data.user)}/>)}
// ^^^ the creation event
// vvv the dynamic part of the timeline. I map each element in events to a timelineEvent
{events && (events.map((event, index) => (
<TimelineEvent
key={index}
createdAt={Moment(event.creationDateTime).fromNow()}
icon={event.type && <ActionIcon variant={event.type} fontSize={18} />}
iconStyle={event.type && this.bubbleStyles(event.type).icon}
bubbleStyle={event.type && this.bubbleStyles(event.type).bubble}
subtitle={this.emptyContainer()}
contentStyle={contStyle}
title={this.titleElement(event, index)}>
{this.showInfoBox(event.type) &&
(<div className={styles.infoBox}>
<div className={styles.infoBoxBody}>
{this.getInfo(event.type)}
</div>
</div>
)}
<div className={styles.commentBox}>
{event.comment}
</div>
</TimelineEvent>
)))}
</Timeline>
null && something resolves to null.
It looks like <Timeline> can't accept null as its props.children. Try to prepare events before rendering <Timeline>:
let allEvents = [];
if (data) allEvents.push(/*something*/)
if (events) {
allEvents = allEvents.concat(events.map(/*something*/));
}
...
{allEvents.length > 0 && (
<Timeline>
{allEvents.map(event => <TimelineEvent/>)}
</Timeline>
)}
Might be a bug https://github.com/rcdexta/react-event-timeline/blob/master/components/Timeline.js#L8
const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { orientation }))
My solution for this is when the value is null, just return an empty div and when you run it, it will work fine.
I think the solution above will cause performance issue since it looped twice.
I hope it helps.
Related
On a web page I wish to display an element which depends on the state of some JavaScript. State like in a state machine. Currently the possible states are these (but I may add more):
input: display some input elements for the user to set. The user can click a button to start some JavaScript processing and move to the working state.
working: display a progress bar informing the user that the script is running. The user can cancel the computation (moving back to the input state) or the computation can end (moving to either the result or error state).
result: display the computation result. The user can go back to input with a button.
error: display the error. The user can go back to input with a button.
The JavaScript part is ready and working, but I'm unsure how to do this in HTML + CSS.
Current solution and its issue
Currently I've been doing it with classes: I set a class to a common ancestor element with the same name of the state and I display the right elements based on it. Something like this:
const parent=document.querySelector("#parent");
let timer=null;
function input(){
parent.classList.remove("working","result","error");
parent.classList.add("input");
}
function run(){
parent.classList.remove("input");
parent.classList.add("working");
timer=setTimeout(result,1500)
}
function stop(){
clearTimeout(timer);
input();
}
function result(){
parent.classList.remove("working");
if(Math.random()>0.5){parent.classList.add("result");}
else{parent.classList.add("error");}
}
input();
#input{display:none;}
#working{display:none;}
#result{display:none;}
#error{display:none;}
#parent.input #input{display:block;}
#parent.working #working{display:block;}
#parent.result #result{display:block;}
#parent.error #error{display:block;}
<div id="parent">
<div id="input">INPUT. RUN</div>
<div id="working">WORKING. STOP</div>
<div id="result">RESULT. RESTART</div>
<div id="error">ERROR. RESTART</div>
</div>
This solution works but it feels unstable: in theory it would be possible for the parent element to have no classes (in which case nothing is displayed) or multiple ones (in which case you'd see multiple states at once). This shouldn't happen, but the only thing preventing it is the correctness of my script.
Question
Are there better ways to implement this idea of states, so that the HTML elements can't end up in inconsistent states?
Let’s consider the role which HTML plays in a state machine on the web. A machine has moving parts, it is dynamic, so the core of any machine on the web must be implemented in Javascript. HTML is useful only to provide the interface between the user and the machine. It’s a subtle distinction but it fundamentally changes the way you write it.
Have you ever used React? React provides the framework to create entire web applications as “state machines”. React’s mantra is “UI is a function of state”. In a React app, you have a single variable which contains the current state, rendering code which builds the UI based on the state, and core code (mostly event handlers) which updates the state.
Even if you don’t want to build in React, you can use the same general idea:
keep the current state in a Javascript variable (typically you’d use an object, but in this case we only need a string)
write a rendering function which reads the state and then builds the appropriate HTML to represent that state
in the event handlers for your links, do any operations which are required, update the state and call the rendering function
let state = null
let timer = null
// core code
const input = () => {
state = 'input'
render()
}
const run = () => {
state = 'working'
render()
timer = setTimeout(result,1500)
}
const stop = () => {
clearTimeout(timer)
state = 'input'
render()
}
const result = () => {
if(Math.random()>0.5)
state = 'result'
else
state = 'error'
render()
}
// rendering code
const render = () => {
let x = state
switch(state) {
case 'input':
x += ' run'
break
case 'working':
x += ' stop'
break
case 'result':
x += ' restart'
break
case 'error':
x += ' restart'
break
}
document.getElementById('container').innerHTML = x
}
// initialisation code
state = 'input'
render()
<div id="container"></div>
I am using a library called lit to create custom web components and i have tried using the #change and #select event handlers to display another component with no luck. I also can't seem to find the info on the docs.
My code looks like this :
return html`
<div>
<bx-select helper-text="Optional helper text" #change=${this._updateValue} label-text="Select" placeholder="HIV Test 1 Results:">
${this.answers?.map(
(item: any) => html`<bx-select-item
label-text=${item.label}
value=${item.concept}
.selected=${this.initialTestVal == item.concept}
>
${item.label}
</bx-select-item>`)}
</bx-select>
<bx-select helper-text="Optional helper text" label-text="Select" placeholder="HIV Test 2 Results:">
${this.answers?.map(
(item: any) => html`<bx-select-item #change=${this._updateValue}
label-text=${item.label}
value=${item.concept}
.selected=${this.confirmedTestVal == item.concept}
>
${item.label}
</bx-select-item>`)}
</bx-select>
<bx-select helper-text="Optional helper text" label-text="Select" placeholder="HIV Test 3 Results:">
${this.answers?.map(
(item: any) => html`<bx-select-item
label-text=${item.label}
value=${item.concept}
.selected=${this.finalTestVal == item.concept}
>
${item.label}
</bx-select-item>`
)}
</bx-select>
</div>`;
Any help/ advise on this will be appreciated.
Based on the name <bx-select> I'll assume you're using Carbon web components.
Unfortunately it doesn't look like it's listed in the doc but the event name that's fired when you select appears to be bx-select-selected so you'd want to add an event listener with #bx-select-selected.
This can be seen here https://web-components.carbondesignsystem.com/?path=/story/components-select--default when you select an option and see the "Actions" tab below.
You can also see the component's source code to see where the event is dispatched here https://github.com/carbon-design-system/carbon-web-components/blob/c318f69d726a72f006befc7aa46b76b33695d07f/src/components/select/select.ts#L62 and the name is defined here https://github.com/carbon-design-system/carbon-web-components/blob/c318f69d726a72f006befc7aa46b76b33695d07f/src/components/select/select.ts#L387.
I want to create a React webpage that has both editable and read-only versions, the whole page not just a few elements on the page. A version is displayed to the user based on user id and other conditions. How do I do it?
The only straight forward way I know is to create 2 pages one editable and one read-only and based on the condition show the appropriate version (html page) to the user.
Is there a better and smarter way to do this? Like can I create just one page for both versions and toggle the mode based on the condition to the users?
Your question should have provided an example of some code you had tried but based on the description, very rough example below of one of many possible solutions.
Suppose EditView component is your page and you are able to pass a value for permission based on whatever credential you need to apply.
Then you have a component, ExampleField that takes the permission and displays either an input or static text. A collection of multiple of these fields is mapped from a theoretical array of data that you'll have to fetch from somewhere and the fields are returned by the main component.
const EditView = ({permission}) => {
const [editable, setEditable] = useState();
const [values, setValues] = useState([]);
useEffect(() => {
setEditable(permission);
}, [permission]);
useEffect(() => {
//maybe fetch your data from a back end or whatever and assign it to `values`
//on page load
}, [])
const ExampleField = ({permission, val, index}) => {
const handleChange = (e) => {
let vals = [...values];
vals[index] = val;
setValues(vals);
}
return(
<>
{permission
? <input name="example" type="text" defaultValue={val}
onChange={handleChange} />
: <span>{val}</span>}
</>
)
}
const fields = values.map((value, i) => {
return <ExampleField permission={permission} val={value} index={i}/>
})
return(
<>
{fields}
</>
)
}
Most likely, you'll want to break out various field components into their own file and, instead of using useState, you would probably want to explore useContext or useStore type functionality to lift up your state and do all the react things.
*Haven't tested or even compiled this code - for illustration purposes only.
So I want to show an icon based on whether or not the number of projects in my list is > 3. I am using this getProjects() function that I need to subscribe to in order to get the data. I am setting a boolean when I subscribe that checks the number of projects in the list, then in my HTML, I use a ngIf to show the icon based on the boolean. I am able to get it to show correctly, however, I think I am constantly polling in my subscribe, and setting this boolean over and over again because it is making my webpage run really slow.
I have already tried the take(1) method which doesnt seem to stop the subscription, as well as set it to a "this.variable" scope inside my component. I am currently using event emitters however that is not working either.
This is my code so far,
Function that I subscribe to (in a different component):
getProjects(): Observable<ProjectInterfaceWithId[]> {
const organizationId = localStorage.getItem('organizationId');
return this.firestoreService.collection('organizations').doc(organizationId)
.collection('projects').snapshotChanges()
.pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as ProjectInterface;
const id = a.payload.doc.id;
return {id, ...data} as ProjectInterfaceWithId;
})),
map(list => {
if (list.length !== 0) {
this.buildProjectLookup(list);
this.projects = list;
return list;
}
})
);
}
Function that i use to get the data and set the boolean:
#Input() toggle: boolean;
#Output() iconStatus = new EventEmitter();
displayIcon() {
this.projectService.getProjects()
.pipe(take(1))
.subscribe(
list => {
if(list.length >= 3){
this.toggle = true;
this.iconStatus.emit(this.toggle);
}
});
}
HTML:
<i *ngIf="displayIcon()" class="material-icons">list</i>
Is there any way for me to literally just check the list length once so I don't get caught in this subscription loop? Thank you in advance!
It looks like it could be happening due to the ngIf referring to the displayIcon() method.
Every time change detection runs within your component, this method will be called. If your component is using default change detection, this will be very often.
see https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/ for more
One way this could be fixed is by making the ngIf refer to a variable instead.
For example, you could set a projects$ observable using
this.projects$ = this.projectService.getProjects()
.pipe(
take(1),
tap(projects => this.iconStatus.emit(projects.length >= 3))
);
This observable should likely be instantiated in your ngOnInit() method.
Then in your template you can use
<i *ngIf="(projects$ | async)?.length >= 3" class="material-icons">list</i>
I have a problem with asynchronous HTTP calls in Angular 4 using typescript/components... I create an array of objects, and in the HTML I have checkboxes next to the objects. Now I want certain objects to be checked, by executing a function in angular. However when I do
(document.getElementById(id) as HTMLInputElement).checked = true;
In my component.ts.
It can't find the element however when I do the same code in a function that executes when you push a button it works. So the problem is that the HTML is not fully loaded when I execute the function. How can I make sure the HTML is fully loaded?
Yeah You shouldn't be manipulating the DOM.
Tag your HTML element in the html using hash.
<input ... #inputname />
Retrieved in the ts controller component.
#ViewChild('inputname') theinput;
Check after view init. ngAfterViewInit if it is checked
ngAfterViewInit() {
...
(this.form as HTMLInputElement).checked
...
}
Consider this as the last option since I wouldn't recommend direct DOM manipulation in Angular. But if you are still facing the issue, use can use my solution as a work around.
In constructor ,
let interval = setInterval(() => {
let flag = self.checkFunction();
if (flag)
clearInterval(interval);
}, 100)
Now create the function
checkFunction() {
if(document.getElementById(id)){
(document.getElementById(id) as HTMLInputElement).checked = true;
return true;
}
return false;
}