How can I pass down props from a parent to children in a slot using Stencil.js? - web-component

I have two components using Stencil (TypeScript), a parent and a child.
In the parent I accept props like textColor, fontFamily, etc. and use them in a function to call them in the return statement as a style. Also, I use another function to declare the props that need to be passed down.
What I'm now failing is to pass the function down to the child which is in a slot. Also I have the feeling that there is a smarter way to handle it with one instead of two functions.
Any help is highly appreciated.
Parent
import { Component, h, Prop } from '#stencil/core';
interface Styles {
[key: string]: string;
}
#Component({
tag: 'my-parent',
shadow: true,
})
export class MyParent {
#Prop() textColor: string;
#Prop() fontFamily: string;
getAccordionProps(): Styles {
return {
color: this.textColor
fontFamily: this.fontFamily
};
}
props(): Styles {
return {
fontColor: this.textColor,
fontWeight: this.fontWeight,
fontFamily: this.fontFamily,
};
}
render() {
return (
<Host>
<div class="some-content" style={this.getAccordionProps()}>
</div>
<slot {...this.props()}/>
</Host>
);
}
}
Child
import { Component, h, Prop, Method } from '#stencil/core';
#Component({
tag: 'my-child',
shadow: true,
})
export class MyChild {
#Prop() textColor: string;
#Prop() fontFamily: string;
render() {
return (
<Host style={this.getAccordionProps()>
{/* Use the <slot> element to insert the text */}
<slot />
</Host>
);
}
}

You may be misunderstanding what a slots are and how they work. A slot in a component is a place where any DOM can be added inside the light DOM of a component. A <slot> element uses only one property/attribute - "name", so applying other properties to the slot element does nothing.
Slotted components and their parent components do not have any special access to each other - only standard DOM just like how a <span> within a <div> would have no special access to each other. So for example a child component does not inherit functions from its parent component. However, it can find its parent in the DOM and call the parent's functions. As in:
export class MyChild {
#Element() hostElement;
...
render() {
return (
<Host style={this.hostElement.parent.getAccordionProps()}>
<slot />
</Host>
);
}
}
The problem with this approach is that my-child is completely dependent on my-parent in order to work properly. It's better to keep separate components separate - don't assume a certain parent or child, instead design the components so they can be used independently. For example, to apply attributes to a slotted element like <my-child> (not the <slot> element itself), you would do that in the DOM not in the parent. For example:
<my-parent text-color="..." font-family="...">
<my-child font-color="..." font-family="...">
...
</my-child>
</my-parent>
If you want to apply properties to a slotted component from the parent, you need to find the component as an element and manipulate it as standard DOM. For example:
private myChildElement;
#Element() hostElement;
export class MyParent {
componentWillLoad() {
const child = this.hostElement.querySelector('my-child');
if (child) {
child.setAttribute('font-color', this.textColor);
...
}
}
...
}
Of course, the problem with this approach is that you may not know how the <my-parent> component is being used - it could have none or more than one <my-child> elements. This is why the DOM example above is preferred.
An alternative is to include <my-child> in the <my-parent> template instead of the slot, as mentioned in comments above:
render() {
return (
<Host>
<div class="some-content" style={this.getAccordionProps()}>
</div>
<my-child {...this.props()}></my-child>
</Host>
);
}
The problem with this approach is that it's not flexible to other usages (other content inside the parent), and may require updating both components if <my-child> is updated. Also, if <my-child> is only ever used this way, then whether it needs to be a separate component is questionable, because you could easily include it in <my-parent>:
render() {
return (
<Host>
<div class="some-content" style={this.getAccordionProps()}>
</div>
{/* formerly my-child */}
<div ...>
<slot />
</div>
</Host>
);
}

Parent
export class MyParent {
private children;
#Element() host: HTMLElement;
#Prop() prop1;
#Prop() prop2;
componentDidLoad() {
this.passPropsToChildren()
}
private passPropsToChildren = () => {
this.children = this.host.querySelectorAll('child');
this.children.forEach(child => {
child['prop1'] = this.prop1;
child['prop2'] = this.prop2;
});
}
render() {
return (
<Host>
<div class="some-content" style={this.getAccordionProps()}></div>
<slot />
</Host>
);
}
}
Child
export class Child {
#Prop() prop1;
#Prop() prop2;
render() {
return (
<Host style={this.getAccordionProps()>
{/* Use the <slot> element to insert the text */}
<slot />
</Host>
);
}
}

Related

Apply separate CSS to one of the multiple instances of same React custom component

I have two custom components called Grid and FieldValue and I use the FieldValue component multiple times on a particular page. I am using a class name called .black for all the FieldValue components. Now, I want to use a different class name called .blue-pointer where the data in FieldValue says view2. please help me understand how to do it.
Components on the page look like below
<Grid>
<FieldValue data={'view1'}/>
<FieldValue data={'view2'}/>
<FieldValue data={'view3'}/>
</Grid>
And the FieldValue is defined as below,
class FieldValue extends React.Component<>{
render(){
<div className="black">
{'testView'}
</div>
}
}
And the CSS is defined as below
.black{
color:#4d546d;
}
.blue-pointer {
color: #0070d2;
cursor: pointer;
}
Use props from your component :
class FieldValue extends React.Component{
render() {
return (
<div className={this.props.data === 'view2' ? 'blue-pointer' : 'black'}>
{'testView'}
</div>
);
}
}
You can define className as a prop, and give it a default value of 'black'.
class FieldValue extends React.Component<> {
static defaultProps = {
className: 'black'
}
render() {
const { className } = this.props
<div className={className}>
{'testView'}
</div>}
}
For the default case, you don't need to change anything:
<FieldValue data={'view1'} /> // Still uses the `black` style.
When you want to change the class name, use:
<FieldValue data={'view2'} className='blue-pointer' />

Passing css styles from React Parent component to its child component

I have React parent component A which has its own scss file a-style.scss. Component B is child of A. A is passing styleInfo object as props which is applied on B.
My question is - is there any way we can define styleObj in a-style.scss instead of defining it inline. I want all styling related info should be in external scss file.
Component A
import "./a-style.scss";
import B from "./B.js";
class A extends Component {
constructor(props) {
super(props);
}
const styleObj = {
backgroundColor: "#F9F9F9",
borderRadius: '2px',
color: "#686868",
};
render() {
return (<B styleInfo={this.styleObj}></B>);
}
}
Component B
class B extends Component {
constructor(props) {
super(props);
}
render() {
return (<div style={this.props.styleInfo}></div>);
}
}
The standard way is to define CSS properties based on class in your scss/css. And then pass className from props in your React component:
class A extends Component {
theme = "themeA";
render() {
return (<B styleInfo={this.theme} />);
}
}
class B extends Component {
styleClass = ["B"];
render() {
const className = styleClass.push(this.props.styleInfo).join(' ');
return (<div className={className} />);
}
}
.themeA {
background-color: #F9F9F9;
border-radius: 2px;
color: #686868;
}
.B {
/* Some style for B component */
}
Why not just import that one file directly into B.js?
Is there any benefit of having it go through a parent, seems like necessary routing to me!
If you do need this, then I would just keep it in JS, as this is what JS is good at, or at least, have JS just do the className switching and, again, just have one css file that is a main style lookup hash!
Best of luck!

How to pass host component's CSS class to children?

I cannot understand how to pass host component CSS class to a children element. I created a custom element:
...
const CUSTOM_INPUT_VALUE_PROVIDER: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true,
}
#Component({
moduleId: module.id,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CUSTOM_INPUT_VALUE_PROVIDER],
selector: 'form-field',
template: `
<div>
<input
(change)="onChange($event.target.value)"
(blur)="onTouched()"
[disabled]="innerIsDisabled"
type="text"
[value]="innerValue" />
</div>
`
})
export class FormFieldComponent implements ControlValueAccessor {
#Input() innerValue: string;
innerIsDisabled: boolean = false;
onChange = (_) => {};
onTouched = () => {};
writeValue(value: any) {
if (value !== this.innerValue) {
this.value = value;
}
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean) {
this.innerIsDisabled = isDisabled;
}
get value(): any {
return this.innerValue;
}
set value(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
this.onChange(value);
}
}
}
And then use it like this in some reactive form:
<form-field formControlName="title"></form-field>
Problem: I added some validation in FormBuilder to title form control and when it not pass validation, Angular add classic css classes to form-field element: ng-pristine ng-invalid ng-touched.
How i can pass this CSS classes from host element to my input element in form-field component?
It is not duplicate of Angular 2 styling not applying to Child Component. Changing Encapsulation does not resolve the problem.
I think there's a way to do what you want by just knowing the angular classes of the hosting elements and not necessarily passing them down.
If so, your work-around would look something like this in the css of the custom form element:
:host.(ng-class)>>>HTMLelement {
property: value
}
Example:
:host.ng-valid>>>div {
border-left: 5px solid #42a948;
}
The ":host" part of this allows us to use the hosting (parent) html elements
The ">>>" is the deep selector that allows us to apply these styles to all children matching selection property (in this case, we're looking for div elements)

FlowType React Context

Is there a way to make React Context type-safe with flow type?
For example :
Button.contextTypes = {
color: React.PropTypes.string
};
Unfortunately, it is inherently not possible because Context is not known at compile time (so I was told).
A bit of a workaround I use is pulling the the context from the consumer at the parent level, and then calling proptypes at the child...
Parent
//parent
class Parent extends component {
render(){
return (
<Consumer>{(context)=>{
const { color } = context
return(
<div>
<Button color={color} />
</div>
)}}</Consumer>
}
Child
//Button
...
Button.contextTypes = {
color: React.PropTypes.string
};
...

Communicating with ReactJS components in different Meteor packages

I am building a web application using ReactJS and Meteor. I am using the method of structuring my application where components are split into separate Meteor packages.
For example. I have a component which renders a tabular menu (using the semantic-ui tab module) and initializes the tabs, then each tab is its own React component.
How would I be able to access the DOM in one component in another component.
Example:
Component = React.createClass({
componentDidMount() {
$('.menu .item').tab({
onVisible: () => {
// I need to call a function here, which is defined in OtherComponent,
// but this jQuery won't run in OtherComponent
}
})
},
render() {
return (
<div className="ui tabular menu">
<div className="active item" data-tab="tab-1">Tab 1</div>
<div className="item" data-tab="tab-2">Tab 2</div>
</div>
/* more code */
<div className="ui active tab" data-tab="tab-1"></div>
<div className="ui tab" data-tab="tab-2"></div>
)
}
}
OtherComponentInDifferentPackage = React.createClass({
componentDiMount() {
$('.menu .item').tab({
onVisible: () => {
// this won't work....
}
})
}
})
You'd need to have a parent component that holds state and can pass that as props to both components. In react you can also pass functions as props, in this case you could pass a function from the parent to the children that will trigger a state change in the parent. To change the state of parent you would call this.props.toggleVisible() from the child.
// Parent Component
toggleVisible() {
this.setState({
visible: !this.state.visible
});
}
getInitialState() {
return {
visible: false
}
}
render() {
return (
<div>
<Component
visible={this.state.visible}
toggleVisible={this.toggleVisible}
/>
<OtherComponentInDifferentPackage
visible={this.state.visible}
toggleVisible={this.toggleVisible}
/>
</div>
);
}
// Child Component
componentWillReceiveProps(nextProps) {
if (nextProps.visible !== this.props.visible) {
// do something
}
}

Resources