Web components: How to work with children? - web-component

I'm currently experimenting with StencilJS to create some web components.
Now I know that there is <slot /> and named slots and all that stuff. Coming from React, I guess slot is similar to children in React. You can do a lot of stuff using children in React. Things I often did:
Check if any children are provided
Iterate over children to do something to each child (e.g. wrap it in a div with a class etc.)
How would you do that using slot/web components/stencilJS?
I can get the Host Element of my web component in Stencil using
#Element() hostElement: HTMLElement;
I use my component like
<my-custom-component>
<button>1</button>
<button>2</button>
<button>3</button>
</my-custom-component>
I want to render something like
render() {
return slottedChildren ?
<span>No Elements</span> :
<ul class="my-custom-component">
slottedChildren.map(child => <li class="my-custom-element>{child}</li>)
</ul>;
}
Kind regards

Using slots you don't need to put a condition in your render function. You can put the no children element (in your example the span) inside the slot element and if no children are provided to the slot it will fall back to it.
For example:
render() {
return (
<div>
<slot><span>no elements</span></slot>
</div>
);
}
Answering the comment you wrote - you can do such a thing but with some coding and not out of the box. Every slot element has an assignedNodes function. Using that knowledge and the understanding of Stencil component life cycle you can do something such as:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
#Element() host: HTMLDivElement;
#State() children: Array<any> = [];
componentWillLoad() {
let slotted = this.host.shadowRoot.querySelector('slot') as HTMLSlotElement;
this.children = slotted.assignedNodes().filter((node) => { return node.nodeName !== '#text'; });
}
render() {
return (
<div>
<slot />
<ul>
{this.children.map(child => { return <li innerHTML={child.outerHTML}></li>; })}
</ul>
</div>
);
}
}
This is not an optimal solution and it will require that the style of the slot should have display set to none (cause you don't want to show it).
Also, it will only work with simple elements that only need rendering and not requiring events or anything else (cause it only uses them as html string and not as objects).

Thank you for the answer Gil.
I was thinking of something similar before (setting state etc. - because of timing issues that might come up). I didn't like the solution though, because you're then doing a state change within componentDidLoad, which will trigger another load just after the component did load. This seems dirty and unperfomant.
The little bit with innerHTML={child.outerHTML} helped me alot though.
It seems like you can also simply do:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
#Element() host: HTMLDivElement;
render() {
return (
<div>
<ul>
{Array.from(this.host.children)
.map(child => <li innerHTML={child.outerHTML} />)}
</ul>
</div>
);
}
}
I thought you might run into timing issues, because during render() the child elements of the host have already been removed to make space for whatever render() returns. But since shadow-dom and light-dom coexist nicely within the host component, I guess there shouldn't be any issues.
I don't really know why you have to use innerHTML though. Coming from React I'm used to doing:
{Array.from(this.host.children)
.map(child => <li>{child}</li>)}
And I thought that is basic JSX syntax and that since Stencil is also using JSX I could do that, too. Doesn't work though. innerHTML does the trick for me. Thanks again.
EDIT: The timing issues I mentioned will appear if you're not using shadow-dom though. Some strange things start to happen an you'll end up with a lot of duplicate children.
Though you can do (might have side effects):
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
children: Element[];
#Element() host: HTMLDivElement;
componentWillLoad() {
this.children = Array.from(this.host.children);
this.host.innerHTML = '';
}
render() {
return (
<div>
<ul>
{this.children.map(child => <li innerHTML={child.outerHTML} />)}
</ul>
</div>
);
}
}

Related

How do you style your React.js components differently depending on where you are using them in your application?

Let's say you have a navbar and when you're using this component on your homepage you want it to have a certain background color and display property, but when you use that same navbar component on another page in your application you want to change these CSS properties. Seeing as the component has one CSS file linked how would you change the style of a component depending on where it is being rendered?
My personal favourite method nowadays is styled components. Your component might look something like this:
// NavBar.js
import styled from 'styled-components'
const StyledDiv = styled.div`
width: 100%;
height: 2rem;
background-color: ${props => props.bgColor};
`
const NavBar = (bgColor) => {
return <StyledDiv bgColor={bgColor}>
}
Then to use it in your different contexts you simply pass the color prop:
// homepage.js
<NavBar bgColor="red" />
// otherpage.js
<NavBar bgColor="#123ABC" />
Styled components are becoming a very popular way of doing things, but be aware that there are a huge array of ways you can do this.
https://styled-components.com/
(Code not tested)
Well If you just want to use plain CSS then you can change the className based on route so the styles also changes.
Example:
import { useLocation } from "react-router-dom";
const Navigation = () => {
let location = useLocation();
...
return(
<nav className={location.pathname === "/home" ? "homepage-navbar" : "default-navbar"}>
...
</nav>
)
}
You can write longer condition for multiple pages as well.
Other better thing you can do is pass the location.pathname and value of className as prop.
import { useLocation } from "react-router-dom";
const Home = () => {
let location = useLocation();
...
return (
<>...
<Navigation location={location.pathname} styleClass={"homepage-navbar"}/>
</>
)
}
const Navigation = ({location, styleClass}) => {
...
return(
<nav className={location === "/home" ? styleClass : "default-navbar"}>
...
</nav>
)
}
So now you can pass different values for className from different components and get different styles for the navbar.

ReactJS advanced custom styling

Using reactjs only, is it possible to do advanced styling similar to
#primary-nav .list-group-item.active {}
// or
#secondary-nav .list-group-item:hover>.background-item {}
in the first example I could do some rather simple javascript logic to figure out if the component is "active" but on the second example it's just so much simpler with css.
Is there a clear react+js solution for these situations that comes close to the simplicity of css?
className is applied exactly like a regular HTML class. So to correctly target .background-image like in
.list-group-item:hover>.background-item
Your jsx structure should look like
import './index.css'
const Component = () =>{
return(
<div className='list-group-item'>
<div className='background-item' />
<span>
<div className='background-item' /> /*Targeting nested items '>' */
</span>
</div>
)
}
You can use jss and clsx to have dynamic and conditional styles. Here is an example using MUI styles(hooks API), but you can use styled components, react-jss or implement you're own style's solution based on jss.
import { makeStyles } from '#material-ui/styles'
import clsx from 'clsx'
const styles = {
root:{
color: 'white',
'&:active':{
color: 'red'
}
},
hidden:{
opacity: 0
}
}
const useStyles = makeStyles(styles)
const Component = ({ open }) =>{
const classes = useStyles()
const rootStyle = clsx({
[classes.root] : true,
[classes.hidden] : !open
})
return <div classsName={rootStyle} />
}
jss also have lots of cool features like theming support, styles interpolation (a personal favorite), nested selectors, style's rules,etc. Definitely worth taking a look.
Notice that clsx doesn't require jss to work, it's just a helper to conditionally apply classes. You can use it like clsx({'foo' : true, 'bar': false})

How to resolve FOUC in React.js

I have built react.js site from create-react-app.
But in production mode, there is FOUC because styles are loaded after html is rendered.
Is there any way to resolve this? I have been searching google for answers, but haven't found proper one yet.
FOUC
FOUC - so called Flash of Unstyled Content can be as very problematic as so many tries of solving this issue.
To the point
Let's consider following configuration of routing (react-router):
...
<PageLayout>
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/example' component={Example} />
<Switch>
</PageLayout>
...
where PageLayout is a simple hoc, containing div wrapper with page-layout class and returning it's children.
Now, let's focus on the component rendering based on route. Usually you would use as component prop a React Compoment. But in our case we need to get it dynamically, to apply feature which helps us to avoid FOUC. So our code will look like this:
import asyncRoute from './asyncRoute'
const Home = asyncRoute(() => import('./Home'))
const Example = asyncRoute(() => import('./Example'))
...
<PageLayout>
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/example' component={Example} />
<Switch>
</PageLayout>
...
to clarify let's also show how asyncRoute.js module looks like:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Loader from 'components/Loader'
class AsyncImport extends Component {
static propTypes = {
load: PropTypes.func.isRequired,
children: PropTypes.node.isRequired
}
state = {
component: null
}
toggleFoucClass () {
const root = document.getElementById('react-app')
if (root.hasClass('fouc')) {
root.removeClass('fouc')
} else {
root.addClass('fouc')
}
}
componentWillMount () {
this.toggleFoucClass()
}
componentDidMount () {
this.props.load()
.then((component) => {
setTimeout(() => this.toggleFoucClass(), 0)
this.setState(() => ({
component: component.default
}))
})
}
render () {
return this.props.children(this.state.component)
}
}
const asyncRoute = (importFunc) =>
(props) => (
<AsyncImport load={importFunc}>
{(Component) => {
return Component === null
? <Loader loading />
: <Component {...props} />
}}
</AsyncImport>
)
export default asyncRoute
hasClass, addClass, removeClass are polyfills which operates on DOM class attribute.
Loader is a custom component which shows spinner.
Why setTimeout?
Just because we need to remove fouc class in the second tick. Otherwise it would happen in the same as rendering the Component. So it won't work.
As you can see in the AsyncImport component we modify react root container by adding fouc class. So HTML for clarity:
<html lang="en">
<head></head>
<body>
<div id="react-app"></div>
</body>
</html>
and another piece of puzzle:
#react-app.fouc
.page-layout *
visibility: hidden
sass to apply when importing of specific component (ie.: Home, Example) takes place.
Why not display: none?
Because we want to have all components which rely on parent width, height or any other css rule to be properly rendered.
How it works?
The main assumption was to hide all elements until compoment gets ready to show us rendered content. First it fires asyncRoute function which shows us Loader until Component mounts and renders. In the meantime in AsyncImport we switch visibility of content by using a class fouc on react root DOM element. When everything loads, it's time to show everything up, so we remove that class.
Hope that helps!
Thanks to
This article, which idea of dynamic import has been taken (I think) from react-loadable.
Source
https://turkus.github.io/2018/06/06/fouc-react/

What is the idiomatic way to create styles for a Reason-React component that depend on props?

To learn reason and reason-react, I'm working on a simple “Things 2 Do” app (see source code on GitHub).
I have a TodoItem component that should be rendered with strike-through style when the item has been completed.
I try to solve this by creating a record with various styles, similar to CSS classes, one root style and one for completed items.
type style = {
root: ReactDOMRe.style,
completed: ReactDOMRe.style
};
let styles = {
root: ReactDOMRe.Style.make(), /* add root styles here */
completed: ReactDOMRe.Style.make(~opacity="0.666", ~textDecoration="line-through", ())
};
If the prop completed is true, I combine the root style with the completed style, otherwise I just use the root, like this:
let style = styles.root;
let style = item.completed ? ReactDOMRe.Style.combine(style, styles.completed) : style;
This works, but it seems clunky, so I'm wondering: Is there a more elegant solution, e.g. using a variant and a switch statement?
What is the idiomatic way to create styles for a Reason-React component that depend on props?
Here is the full code of my component:
type item = {
id: int,
title: string,
completed: bool
};
type style = {
root: ReactDOMRe.style,
completed: ReactDOMRe.style
};
let str = ReasonReact.stringToElement;
let component = ReasonReact.statelessComponent("TodoItem");
let styles = {
root: ReactDOMRe.Style.make(), /* add root styles here */
completed: ReactDOMRe.Style.make(~opacity="0.666", ~textDecoration="line-through", ())
};
let make = (~item: item, ~onToggle, _) => {
...component,
render: (_) => {
let style = styles.root;
let style = item.completed ? ReactDOMRe.Style.combine(style, styles.completed) : style;
<div style>
<input
_type="checkbox"
onChange=((_) => onToggle())
checked=(Js.Boolean.to_js_boolean(item.completed))
/>
<label> (str(item.title)) </label>
</div>
}
};
I don't think there's anything that can be called idiomatic yet. The area is quickly changing, and even I have some ideas of my own on how to improve it, but this is more or less how I do it now using bs-css:
module Styles = {
open Css;
let root = completed => style([
color(white),
opacity(completed ? 0.666 : 1.),
textDecoration(completed ? LineThrough : None)
]);
}
...
render: _self =>
<div className=Styles.root(item.completed)>
...
</div>
For now, the way I'm styling my component is OK. There is not really an idiomatic way for styling React components in Reason yet.
The Reason documentation has this to say:
Since CSS-in-JS is all the rage right now, we'll recommend our official pick soon. In the meantime, for inline styles, there's the ReactDOMRe.Style.make API

Meteor React - Why are React Components defined differently in React Mounter vs React Layout from Kadira?

I am looking forward to Meteor 1.3 so I can import React components instead of having them as globals.
Been following this tutorial (https://voice.kadira.io/getting-started-with-meteor-1-3-and-react-15e071e41cd1) and I noticed I will have to use React-mounter instead of React-Layout from Kadira
In these docs here:
https://github.com/kadirahq/react-mounter
I see that the React components are defined like this:
const MainLayout = ({content}) => (
<div>
<header>
This is our header
</header>
<main>
{content}
</main>
</div>
);
Instead of something like this
MainLayout = React.createClass({
propTypes: {
content: React.PropTypes.element
},
render() {
return (
<div>
<header>
This is our header
</header>
<main>
{this.content}
</main>
</div>
);
}
});
Can you help explain to me what is happening here? Also how do I use this new style? Where to define all the properties, methods, mixins, etc?
Also as a side question, I noticed React was added as an npm package, instead of using Meteor add react. Is this how we are supposed to add react now?
You could categorize your components in two types: containers and presentational components.
For more details see this
React v0.14 introduced something called functional components which are presentation components that are created via a function instead of a class instance.
Since they are presentational components they are not intended to have more methods or mixins or anything, they just display data.
If you want to stick with React v0.14 and ES2015 you could create your components like
class Component extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('componentWillReceiveProps', nextProps.data.bar);
}
render() {
return <div>Bar {this.props.data.bar}!</div>;
}
}
You now have a full component that can have state, other event handlers and other methods.
A very important thing to note here is that the ES2015 syntax does not allow mixins because they prefer inheritance or functional composition.
Hope that helps!
Sorry I can't help you with your side question, haven't use React with Meteor.

Resources