Stencil: Rendering Dynamic Attributes in WebComponent - stenciljs

Implementing dynamic Attributes within a Tag seems not possible.
What i want to achieve is following:
I want to define my own select-component, render code as follows:
render() {
return (
<Host>
<select class={this.cssClassMap} aria-label={this.label}>
{
this.options.map((opt) => {
const Tag = 'option';
return <Tag value={this.extractValue(opt)}>{this.writeElement(opt, ['value'])}</Tag>;
})
}
</select>
</Host>
);
}
Whilist the const "Tag" can be defined dynamically as string i have no chance to render dynamically zero or more attributes, here in this example it's "value".
What i want to achieve is something like:
this.options.map((opt) => {
const Tag = 'option';
return <Tag determineAttributeList(opt)>{this.writeElement(opt, ['value'])}</Tag>;
})
I can't find anything on this.
Should be the latest stencil version Date 15.02.

If you want to dynamically add attributes/properties you can use an object and destructure it:
const determineAttributeList = (option) => {
return {
value: option.value,
customAttr: option.custom,
};
}
return (
<select>
{this.options.map((opt) => {
const Tag = 'option';
return <Tag {...determineAttributeList(opt)}>{this.writeElement(opt, ['value'])}</Tag>;
})
</select>
);
Note the {... } around the determineAttributeList function call.

Related

How can I access to each element inside of a grid element with Cypress?

I have a Grid component which includes 24 divs and inside of each div I need to take the value.
This value actually arrives in <p>, so which is the best way to do this?
Below is the app image. I would appreciate an example.
You could probably do something like storing the data in an object or array outside of the Cypress chain. Without a code example, here's my best guess.
cy.get('grid').within(() => { // cy.within() searches only within yielded element
const data = {}; // create data object
cy.get('div').each(($div, index) => { // cy.each() allows us to iterate through yielded elements
data[index] = $div.text() // or possibly some other JQuery command to get the value
// additionally, could go without the index at all and make `data` an array.
}).then(() => {
// whatever needs to be done with `data`, wrapped in `then` to make sure data is populated correctly.
});
});
You can add use each for this to loop through the elements and then do further operations:
cy.get('.chakra-stack')
.find('p')
.each(($ele) => {
cy.log($ele.text().trim()) //Logs all the texts one by one
})
Just add the p selector to your cy.get() command
cy.get('div.chakra-stack p') // access <p> within <div>
.each($el => {
cy.wrap($el).invoke('text')
.then(text => {
...
})
})
To get the value before the text
cy.get('div.chakra-stack p') // access <p> within <div>
.each($el => {
cy.wrap($el)
.prev() // element with value
.invoke('text')
.then(value => {
...
})
})
Accessing values by text label like this
const values = {}
cy.get('div.chakra-stack p')
.each($el => {
const frase = $el.text()
cy.wrap($el).prev().invoke('text')
.then(value => values[frase] = +value)
})
.then(() => {
// values = {'shield': 1, 'session': 2, ...}
})

Export child components and use individually

I have a case where I need to export two child components and use individually.
Much Desired outcome (Extremely simplified):
Controls.js:
const Controller = ( props ) => {
const ControlBoxes = () => {
return(<Button>Move around!</Button>)
}
const MoveableBox = () => {
return(<View>I will be moved! </View>)
}
return {ControlBoxes, MoveableBox}
}
export default Builder
Canvas.js:
import Controller from './controls'
const boxScaleMove = boxes.map((box, index) => {
return (
<Bulilder.MoveableBox key={box.id} box={box}/>
)
}
const boxController = boxes.map((box, index) => {
return (
<Bulilder.ControlBoxes key={box.id} box={box}/>
)
}
return (
...
{boxController}
...
...
{boxScaleMove}
...
)
Any idea how I can achieve this or am I missing something fundamental? The main issue is that I want to avoid resorting to useContext (due to performance reasons in the case of a lot of boxes rendered) and be able to share variables and states between MoveableBox-component and ControlBoxes-component via Controller -parent.
Any help would be greatly appreciated.
You could use the compound component and use a lower level context to avoid re-rendering of the whole tree and share states across your components that way, below I would ilustrate a basic example of how that would work.
const RandomContext = createContext();
export default function Controller({children, ...rest}) {
const [randomState, setRandom] = useState(0);
return (
<RandomContext.Provider value={{ randomState, setRandom }}>
<div {...rest}>{children}</div>
</RandomContext.Provider>
);
}
Controller.ControlBoxes = function (props) {
const { setRnadom } = useContext(RandomContext);
return (
<Button onClick={() => setRandom(2)} {...props}>Move around!</Button>
);
};
Controller.MoveableBox = function (props) {
const { randomState } = useContext(RandomContext);
return randomState ? <View {...props}>I will be moved!</View> : null;
};
And you would use it as:
<Controller>
<Controller.ControlBoxes />
<Controller.MoveableBox />
<Controller>
In the compound components pattern we are leveraging the fact that in javascript when you declare a function you create a function/object combo. Therefor Controller function is both a function and an object, so we can assign properties the the object part of that combo, properties which are in our case ControlBoxes and MoveableBox which are functions themselves.
NOTE you should probably assign named function the the properties of that object, it's easier to debug if the case needed.
Example.Function = function ExampleFunction(props) {
return "Example";
};

Auto Calculate Input Fields

Please could someone help answer this:
I have 2 NumberInput controls, one input and the other is disabled. I need to input number in the first input field, the disabled field to show this number/100. The two NumberInput will have source fields that will save to the current record in the simpleform.
How do I do this in react-admin
Thanks
Easiest way is to use the method described in the docs under section Linking two inputs
In essence: You can create your own input component where you can access the form values via the hook useFormState. Then just assign the desired value transformed the way you want e.g. divided by 100.
Edit
Found one more even cleaner way - using the final-form-calculate to create a decorator and pass it to the <FormWithRedirect /> component like so:
import createDecorator from 'final-form-calculate'
const calculator = createDecorator(
// Calculations:
{
field: 'number1', // when the value of foo changes...
updates: {
number2: (fooValue, allValues) => allValues["number1"] * 2
}
})
...
<FormWithRedirect
...
decorators={[calculator]}
/>
Check out this code sandbox
Using FormDataConsumer
<FormDataConsumer>
{({ formData }) => (
<NumberInput defaultValue={formData.my_first_input / 100} source="second_input"/>
)}
</FormDataConsumer>
Using the useFormState hook
import { useFormState } from 'react-final-form';
...
const { values: { my_first_input }} = useFormState({ subscription: { values: true } });
...
<NumberInput defaultValue={my_first_input / 100} source="second_input"/>
Source: https://marmelab.com/react-admin/Inputs.html#linking-two-inputs
Dynamic
You need to use the useForm hook of react-final-form to make your input dynamic:
import { useForm, useFormState } from 'react-final-form';
...
const {change} = useForm();
const { values: { my_first_input }} = useFormState({ subscription: { values: true } });
useEffect(() => {
change('my_second_input', my_first_input / 100);
}, [change, my_first_input]);
...
<NumberInput defaultValue={my_first_input / 100} source="second_input"/>
I got a shorter solution to this question:
All I did was to do the calculation within FormDataConsumer. Now, I am able to get the calculated value and it updates the correct record in the array.
Thanks
<FormDataConsumer>
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
getSource, // A function to get the valid source inside an ArrayInput
...rest
}) => {
if (typeof scopedFormData !== 'undefined') {
scopedFormData.total = scopedFormData.quantity * scopedFormData.unitprice;
return (
<NumberInput disabled defaultValue={scopedFormData.total} label="Total" source={getSource('total')} />
)
} else {
return(
<NumberInput disabled label="Total" source={getSource('total')} />
)
}
}}

Store result from api call React native

How can I store the recipe name from this api call into a state variable or some other variable to be used further down in the render
{
this.props.recipeList.recipeList.filter((recipe) => recipe.recipeName === search).map(recipe => {
return <Recipe
name={recipe.recipeName}
numberOfServings={recipe.numberOfServings}
key={'recipe-' + recipe.recipeId}
/>
})
}
As this is performed within render, avoid modifying the component state.
I would recommend declaring a variable at the top of your render method and then assigning it in your map call. This, of course, assumes that filter will only ever return a single result.
Something like:
render() {
let name;
....
{
this.props.recipeList.recipeList.filter((recipe) => recipe.recipeName === search).map(recipe => {
name = recipe.recipeName; // this bit
return <Recipe
name={recipe.recipeName}
numberOfServings={recipe.numberOfServings}
key={'recipe-' + recipe.recipeId}
/>
})
}
....

How to Call Function on Each dom-repeat Element

I am trying to call a function on each element inside of a dom-repeat template.
<dom-repeat items="[[cart]]" as="entry">
<template>
<shop-cart-item id="item"></shop-cart-item>
</template>
</dom-repeat>
...
checkStatus() {
this.$.item.doSomething();
}
How can I call doSomething on each element?
You can iterate through nodes like:
checkStatus() {
const forEach = f => x => Array.prototype.forEach.call(x, f);
forEach((item) => {
if(item.id == 'cartItem') {
console.log(item);
item.doSomething(); // call function on item
}
})(this.$.cartItems.childNodes)
}
You can add an on-tap event in the loop. In order to observe which item you clicked look into model property :
<dom-repeat items="[[cart]]" as="entry">
<template>
<!-- need to give dynamic id for each item in dome-repeat -->
<shop-cart-item id="[[index]]" on-tap = 'checkStatus'></shop-cart-item>
</template>
</dom-repeat>
...
checkStatus(status) {
console.log(status.model) // you can get index number or entry's properties.
this.$.item.doSomething();
}
EDIT:
So as per comment of #Matthew, if need to call a function in one of element's function in dom-repeat first give a dynamic id name as above then:
checkStatus(status) {
this.shadowRoot.querySelector('#'+ status.model.index).doSomething();
}