Populating a select form by iterating through an object - forms

Attempting to populate a select form using an object I'm passing to my component as a prop. The object looks like this: {24: {14: 64.99, 20: 89.99, 26: 114.99}, 30: {14: 74.99, 20: 99.99, 26: 124.99} and I'm attempting to isolate the 24 and the 30 as values in my form. Here's the relevant code:
import React, { Component } from 'react';
import { Row, Input } from 'react-materialize';
class HeightPicker extends Component {
constructor(props){
super(props);
this.state = {
height: '',
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({height: e.target.value});
}
displayHeights(){
let prices = this.props.prices;
let height;
let heights = [];
console.log(prices);
for (height in prices) {
heights.push(height)
};
console.log(heights);
heights.forEach(function(h) {
return(<option value={h}>{h}</option>);
});
}
render() {
return(
<div>
<Row>
<Input l={12} value={this.state.height} onChange={this.handleChange} type='select' label="Height">
{this.displayHeights()}
</Input>
</Row>
</div>
)
};
};
export default HeightPicker;
As constructed above, it's returning a blank form. If I hardcode options into my render function it works, therefore I'm assuming my issue is arising through my displayHeights function. But I also was running into some issues earlier with React Materialize and the version of React I was running -- had to downgrade versions from 16.0.0 to 15.6.2 -- so I'm wondering if it's related to that. Any help would be appreciated. Thanks.

Use map instead of forEach in displayHeights method
forEach method does some operation on each element of array or collection, but does not return the modified element, map method returns the modified element after some operation
Your implemenetation has two issues
you are using forEach which does not return the modified elements
you did not return the array containing options, in your case , heights
The modified code block will be
return heights.map(function(h) {
return(<option value={h}>{h}</option>);
});

This should be your displayHeights function. Here is a WORKING DEMO
You need to return the array back in your function and also map is what you should be using.
displayHeights(){
let prices = this.props.prices;
let height;
let heights = [];
console.log(prices);
for (height in prices) {
heights.push(height)
};
console.log(heights);
return heights.map(function(h, i) {
return(<option key={i} value={h}>{h}</option>);
});
}
Using forEach
let heightsJSX = [];
heights.forEach(function(h, i) {
heightsJSX.push(<option key={i} {value={h}>{h}</option>));
});
return heights;

For efficiency's sake, why not just map over the keys. Here's a one-liner.
displayHeight() {
return Object.keys(this.props.prices).map(key => <option key={key} value={key}>{key}</option>);
}

Related

LWC: Sibling component not rerendering after Parent is updated

I have an LWC which acts as a ToolBox: a user selects a tool from the ToolBelt and then a WorkArea is populated with the business logic for that tool.
Components involved:
Parent: Wrapper.js
Child 1: ToolBelt.js
Child 2: WorkArea.js
Where things are working properly: First, Wrapper.js passes a ToolSelection handler down to ToolBelt.js. On select, an event is emitted from ToolBelt.js to Wrapper.js where the selectedTool variable is being updated.
Where things are not working properly: selectedTool is decorated with #api in the parent, #track in the child, and the value is being successfully updated in the parent. But it is not being rerendered in the child component.
Parent.js:
import { LightningElement, api } from 'lwc';
export default class Toolbox extends LightningElement {
#api selectedTool
#api tools = [redacted]
toolSelectionHandler(event){
let updatedTools
let selectedTool;
const id = event.detail.id
const action = event.detail.action
if( action === 'unselect'){
updatedTools = this.tools.map( (tool) => {
tool.selected = false
return tool
})
this.selectedTool = null
} else{
updatedTools = this.tools.map( (tool) => {
if(tool.id === id){
tool.selected = true
selectedTool = tool
}
else {
tool.selected = false
}
return tool
})
this.selectedTool = selectedTool
}
this.tools = updatedTools
}
}
Parent.html:
<template>
<div class="slds-grid slds-wrap slds-grid--pull-padded">
<div class="slds-p-horizontal--small slds-size--1-of-2 slds-medium-size--1-of-6 slds-large-size--4-of-12" >
<c-toolbelt
tools={tools}
ontoolselected={toolSelectionHandler}
></c-toolbelt>
</div>
<div class="slds-p-horizontal--small slds-size--1-of-2 slds-medium-size--5-of-6 slds-large-size--8-of-12">
<c-work-area
selected-tool={selectedTool}
>
</c-work-area>
</div>
</div>
Leaving out Toolbelt.js and Toolbelt.html because the selection handler is working as expected.
WorkArea.js:
import { LightningElement, track } from 'lwc';
export default class WorkArea extends LightningElement {
#track selectedTool
#track isLoading = false
get tool1(){
let matchBool
if(!this.selectedTool){
matchBool = false
} else {
if (this.selectedTool.title = 'tool1') {
matchBool = true
}
}
return matchBool;
}
get tool2(){
let matchBool
if(!this.selectedTool){
matchBool = false
} else {
if (this.selectedTool.title = 'tool2') {
matchBool = true
}
}
return matchBool;
}
get tool3(){
let matchBool
if(!this.selectedTool){
matchBool = false
} else {
if (this.selectedTool.title = 'tool3') {
matchBool = true
}
}
return matchBool;
}
spinnerHandler(){
this.isLoading = !this.isLoading
}
}
WorkArea.html:
<template>
<div class="work-area">
<div if:true={toolOne} class="tool-detail-area selected">
<c-tool-one
onspinnerhandler={spinnerHandler} >
</c-tool-one>
</div>
<div if:true={toolTwo} class="tool-detail-area selected">
<c-tool-two
onspinnerhandler={spinnerHandler} >
</c-tool-two>
</div>
<div if:true={toolThree} class="tool-detail-area selected">
<c-tool-three
onspinnerhandler={spinnerHandler} >
</c-tool-three>
</div>
<div if:false={selectedTool} class="tool-detail-area default">
<c-no-tool-display></c-no-tool-display>
</div>
<div if:true={isLoading}>
<c-loading-spinner></c-loading-spinner>
</div>
</div>
I've seen a few SO posts about LWC Child components not registering changes made to parent, but most of them are Parent > Child relationship directly. And because events aren't emitted from the child. I haven't seen any where a child modifies state of parent, and tracked variable in sibling isn't re-rendering.
Any help technically or conceptually would be appreciated.
(Converted from comments / expanded)
Why WorkArea's selectTool is just #track and not #api? Do you manage to pass anything at all to the child that way? I'm bit surprised. If it's a primitive (String, number etc) you shouldn't even need track.
It has to be #api to be part of the component's public interface so I think you setting the value on parent didn't have any effect. That variable was child component's private matter, nice try parent.
in parent's event handler try to rebuild the variable, this.selectedTool = JSON.parse(JSON.stringify(this.selectedTool)); and see if it helps
This one's complicated and reeks of cargo cult programming. I don't know. Rebuilding the variable (object or array of object) sometimes helps the #track / #api realise the value changed and propagate it properly. There's some caching / saving on network roundtrips at play.
https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.reactivity_fields
It shouldn't be needed, #track should be sufficient... But people struggle with it, you can see the trick on some blog posts or
https://salesforce.stackexchange.com/q/274011/799
https://salesforce.stackexchange.com/q/354052/799
JSON.parse "dance" is OK but of course you'll lose any functions you might have had attached to the object, Dates will flatten to strings... It's quite common to use it when debugging something in JS console and you're getting angry with all the Proxy objects. Array spread operator works OK too for giving the #track/#api a nudge, probably faster execution too.
Another place where it helps is when you need to modify what was sent from Apex. Normally that object is readonly, you need the JSON.parse or spread to make a copy(?). For example good luck using <lightning-tree-grid> with any data coming from server-side. It requires child nodes to be _children but Apex doesn't compile with variable names starting with underscore. So you need to get your data and then decorate it a bit in JS: https://salesforce.stackexchange.com/a/235214/799

Dynamic tag name in Solid JSX

I would like to set JSX tag names dynamically in SolidJS. I come from React where it is fairly simple to do:
/* Working ReactJS Code: */
export default MyWrapper = ({ children, ..attributes }) => {
const Element = "div";
return (
<Element {...attributes}>
{children}
</Element>
)
}
but when I try to do the same thing in SolidJS, I get the following error:
/* Console output when trying to do the same in SolidJS: */
dev.js:530 Uncaught (in promise) TypeError: Comp is not a function
at dev.js:530:12
at untrack (dev.js:436:12)
at Object.fn (dev.js:526:37)
at runComputation (dev.js:706:22)
at updateComputation (dev.js:691:3)
at devComponent (dev.js:537:3)
at createComponent (dev.js:1236:10)
at get children [as children] (Input.jsx:38:5)
at _Hot$$Label (Input.jsx:7:24)
at #solid-refresh:10:42
I would like to know if I miss something here, or whether it is possible to achieve this in SolidJS in any other way.
Solid has a <Dynamic> helper component for that use.
import {Dynamic} from "solid-js/web";
<Dynamic component="div" {...attributes}>
{props.children}
</Dynamic>
Here is an alternative implementation covering simple cases like strings and nodes although you can extend it to cover any JSX element:
import { Component, JSXElement} from 'solid-js';
import { render, } from 'solid-js/web';
const Dynamic: Component<{ tag: string, children: string | Node }> = (props) => {
const el = document.createElement(props.tag);
createEffect(() => {
if(typeof props.children === 'string') {
el.innerText = String(props.children);
} else if (props.children instanceof Node){
el.appendChild(props.children);
} else {
throw Error('Not implemented');
}
});
return el;
};
const App = () => {
return (
<div>
<Dynamic tag="h2">This is an H2!</Dynamic>
<Dynamic tag="p">This is a paragraph!</Dynamic>
<Dynamic tag="div"><div>Some div element rendering another div</div></Dynamic>
</div>
)
}
render(App, document.body);
This works because Solid components are compiled into native DOM elements, however since we do not escape the output, it is dangerous to render any children directly, given that you have no control over the content.
This alternative comes handy when you need to render rich text from a content editable or a textarea, text that includes tags like em, strong etc. Just make sure you use innerHTML attribute instead of innerText.

Vuetify TreeView + Drag and drop

I am trying to implement drag and drop on Vuetify Treeview and data table. It seems like it is not supported fully but a workaround is described in this thread. The workaround is however not complete. Perhaps the community would benefit if someone created a codepen or similar on this?
What confuses me is that the component DragDropSlot.vue is created but "drag-drop-slot" is used in the code. Also there is a "_.cloneDeep(this.tree)" call where _ is not defined. I assume it should be replaced by something. When I comment that out drag and drop does still not work. Probably missed something more like defining data. Not sure of correct data types. It seems to be based on react which I have not worked with. Have just started to learn vue and vuetify.
I'm open for any suggestion for how to solve this.
All the best
I use V-Treeview with Vue.Draggable (https://github.com/SortableJS/Vue.Draggable).
I use direct link.
<script src="//cdn.jsdelivr.net/npm/sortablejs#1.8.4/Sortable.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0 vuedraggable.umd.min.js"/>
<v-treeview
:active.sync="active"
:items="users"
:search="search"
item-key="Id"
item-text="UserName"
item-children="Children"
:open.sync="open"
activatable
color="warning"
dense
transition
return-object
>
<template v-slot:label="{ item }">
<draggable :list="users" group="node" :id="item.Id" :data-parent="item.ParentId" #start="checkStart" #end="checkEnd" >
<label>
<i class="fas fa-user mr-3" />
<span id="item.id" >{{item.UserName}}</span>
</label>
</draggable>
Also I add ParentId property to item tree model:
{
Id:1,
UserName: "John Doe",
ParentId: null,
Children:[{Id:2, ParentId: 1,...}]
}
Then I use start and end events where I search parent start node from I drag the item and parent end node where I drop the item. When parent is null the item is a root.
new Vue({
el: '#app',
vuetify: new Vuetify(),
components: {
vuedraggable
},
data() {
return {
active: [],
open: [],
users: [],
selectedItems: [],
}
},
mounted: function () {
this.fetchUsers();
},
methods: {
findTreeItem: function (items, id) {
if (!items) {
return;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
// Test current object
if (item.Id === id) {
return item;
}
// Test children recursively
const child = this.findTreeItem(item.Children, id);
if (child) {
return child;
}
}
},
checkStart: function (evt) {
var self = this;
self.active = [];
self.active.push(self.findTreeItem(self.users, evt.from.id))
},
checkEnd: function (evt) {
var self = this;
var itemSelected = self.active[0];
var fromParent = itemSelected.ParentId ? self.findTreeItem(self.users, itemSelected.ParentId) : null;
var toParent = self.findTreeItem(self.users, evt.to.id);
var objFrom = fromParent ? fromParent.Children : self.users;
objFrom.splice(objFrom.indexOf(itemSelected), 1);
if (toParent.Id === itemSelected.Id) {
itemSelected.ParentId = null;
self.users.push(itemSelected);
}
else {
itemSelected.ParentId = toParent.Id;
toParent.Children.push(itemSelected);
}
self.saveUser(itemSelected);
// self.active = [];
return false;
},
fetchUsers: function () {
//load from api
},
saveUser: function (user) {
//save
},
},
computed: {
selected() {
if (!this.active.length) return undefined
return this.active[0];
},
}
})
Hope I help you.
IngD.
After some additional work I ended up with implementing Drag and Drop on top of vuetify tree view and data table using this library:
https://www.vuetoolbox.com/projects/vue-drag-drop
At first I looked at draggable and similar but realized it was always based on that you move an element from position A to position B. I needed more control. For example I wanted the element to disappear when dropping on some drop zones.
found this component.
https://vuejsexamples.com/vuetify-draggable-v-treeview-component/
I didn't try it myself (because it has too few options), but it looks working well in demo.
Anyways, just to try

How to remove certain elements before taking screenshot?

I am able to take screenshot of the page using the example code below:
html2canvas(document.body, {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
Now there are certain div's i dont want to be part of the page when I take the screenshot?
How can i prevent them from being part of the screenshot.
One way I thought was to clone the element and then remove the elements, but taking a screenshot of the clone gives a white screen. Here is the code I used:
html2canvas($(document.body).clone()[0], {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
Add this attribute: data-html2canvas-ignore to any element you don't want to be taken when the screenshot is processed.
Hopefully this will help the next guy.
When I used this library I faced a problem that the lib download all the images in my application, that cause the application to run slowly. I resolved the problem using the ignoreElements option.
This is my code:
var DropAreaElement= document.getElementById("123");
var config= {
useCORS: true,
ignoreElements: function (element) {
if (element.contains(DropAreaElement) || element.parentElement.nodeName =="HTML" || element == DropAreaElement || element.parentNode == DropAreaElement) {
console.log("elements that should be taken: ", element)
return false;
}else {
return true;
}
}
};
html2canvas(DropAreaElement, config).then(function (canvas){
var imgBase64 = canvas.toDataURL('image/jpeg', 0.1);
console.log("imgBase64:", imgBase64);
var imgURL = "data:image/" + imgBase64;
var triggerDownload = $("<a>").attr("href", imgURL).attr("download", "layout_" + new Date().getTime() + ".jpeg").appendTo("body");
triggerDownload[0].click();
triggerDownload.remove();
}).catch(Delegate.create(this, function (e){
console.error("getLayoutImageBase64 Exception:", e);
});
If you don't want to use an attribute, html2canvas does provide a method to remove elements. For example:
html2canvas( document.body, {
ignoreElements: function( element ) {
/* Remove element with id="MyElementIdHere" */
if( 'MyElementIdHere' == element.id ) {
return true;
}
/* Remove all elements with class="MyClassNameHere" */
if( element.classList.contains( 'MyClassNameHere' ) ) {
return true;
}
}
} ).then( function( canvas ) {
document.body.appendChild( canvas );
} );
For more information, see html2canvas options.
You can create HOC for <Printable/> and <NonPrintable/> , you can wrap your component with <NonPrintable><YourCoolComponent/></NonPrintable>
those children components would be excluded.
import React from "react"
interface INonPrintable {
children: React.ReactChildren
}
/*
HOC - Printable which injects the printId to the React component
which gets us Printable Context to html2canvas => jsPDF
eg:
<Printable printId="about-you-print">
<PersonalInfo badEmail={badEmail} />
<IdentityInfo />
<AdditonalInfo />
<AddressInfo
serviceAddress={serviceAddress}
billingAddress={this.state.billingAddress}
setBillingAddress={this.setBillingAddress}
/>
</Printable>
*/
export default function Printable({ printId = "", children, ...restProps }) {
return <div print-id={printId} {...restProps}>{children}</div>
}
/*
HOC - NONPrintable which injects the data-html2canvas-ignore to the React component
which gets us Printable Context to html2canvas => jsPDF
eg:
<NonPrintable style={{display:"flex",justifyContent:'space-around'}}>
<Button
text="Print PDF using Own utility"
onClick={this.handlePrintPdf}
/>
<Button
text="Print PDF using html2canvas + jsPDF"
onClick={this.handlePrintwithPDFjs}
/>
</NonPrintable>
*/
export const NonPrintable = ({ children, ...restProps }) => {
return <div data-html2canvas-ignore {...restProps}>{children}</div>
}

Handle selected event in autocomplete textbox using bootstrap Typeahead?

I want to run JavaScript function just after user select a value using autocomplete textbox bootstrap Typeahead.
I'm searching for something like selected event.
$('.typeahead').on('typeahead:selected', function(evt, item) {
// do what you want with the item here
})
$('.typeahead').typeahead({
updater: function(item) {
// do what you want with the item here
return item;
}
})
For an explanation of the way typeahead works for what you want to do here, taking the following code example:
HTML input field:
<input type="text" id="my-input-field" value="" />
JavaScript code block:
$('#my-input-field').typeahead({
source: function (query, process) {
return $.get('json-page.json', { query: query }, function (data) {
return process(data.options);
});
},
updater: function(item) {
myOwnFunction(item);
var $fld = $('#my-input-field');
return item;
}
})
Explanation:
Your input field is set as a typeahead field with the first line: $('#my-input-field').typeahead(
When text is entered, it fires the source: option to fetch the JSON list and display it to the user.
If a user clicks an item (or selects it with the cursor keys and enter), it then runs the updater: option. Note that it hasn't yet updated the text field with the selected value.
You can grab the selected item using the item variable and do what you want with it, e.g. myOwnFunction(item).
I've included an example of creating a reference to the input field itself $fld, in case you want to do something with it. Note that you can't reference the field using $(this).
You must then include the line return item; within the updater: option so the input field is actually updated with the item variable.
first time i've posted an answer on here (plenty of times I've found an answer here though), so here's my contribution, hope it helps. You should be able to detect a change - try this:
function bob(result) {
alert('hi bob, you typed: '+ result);
}
$('#myTypeAhead').change(function(){
var result = $(this).val()
//call your function here
bob(result);
});
According to their documentation, the proper way of handling selected event is by using this event handler:
$('#selector').on('typeahead:select', function(evt, item) {
console.log(evt)
console.log(item)
// Your Code Here
})
What worked for me is below:
$('#someinput').typeahead({
source: ['test1', 'test2'],
afterSelect: function (item) {
// do what is needed with item
//and then, for example ,focus on some other control
$("#someelementID").focus();
}
});
I created an extension that includes that feature.
https://github.com/tcrosen/twitter-bootstrap-typeahead
source: function (query, process) {
return $.get(
url,
{ query: query },
function (data) {
limit: 10,
data = $.parseJSON(data);
return process(data);
}
);
},
afterSelect: function(item) {
$("#divId").val(item.id);
$("#divId").val(item.name);
}
Fully working example with some tricks. Assuming you are searching for trademarks and you want to get the selected trademark Id.
In your view MVC,
#Html.TextBoxFor(model => model.TrademarkName, new { id = "txtTrademarkName", #class = "form-control",
autocomplete = "off", dataprovide = "typeahead" })
#Html.HiddenFor(model => model.TrademarkId, new { id = "hdnTrademarkId" })
Html
<input type="text" id="txtTrademarkName" autocomplete="off" dataprovide="typeahead" class="form-control" value="" maxlength="100" />
<input type="hidden" id="hdnTrademarkId" />
In your JQuery,
$(document).ready(function () {
var trademarksHashMap = {};
var lastTrademarkNameChosen = "";
$("#txtTrademarkName").typeahead({
source: function (queryValue, process) {
// Although you receive queryValue,
// but the value is not accurate in case of cutting (Ctrl + X) the text from the text box.
// So, get the value from the input itself.
queryValue = $("#txtTrademarkName").val();
queryValue = queryValue.trim();// Trim to ignore spaces.
// If no text is entered, set the hidden value of TrademarkId to null and return.
if (queryValue.length === 0) {
$("#hdnTrademarkId").val(null);
return 0;
}
// If the entered text is the last chosen text, no need to search again.
if (lastTrademarkNameChosen === queryValue) {
return 0;
}
// Set the trademarkId to null as the entered text, doesn't match anything.
$("#hdnTrademarkId").val(null);
var url = "/areaname/controllername/SearchTrademarks";
var params = { trademarkName: queryValue };
// Your get method should return a limited set (for example: 10 records) that starts with {{queryValue}}.
// Return a list (of length 10) of object {id, text}.
return $.get(url, params, function (data) {
// Keeps the current displayed items in popup.
var trademarks = [];
// Loop through and push to the array.
$.each(data, function (i, item) {
var itemToDisplay = item.text;
trademarksHashMap[itemToDisplay] = item;
trademarks.push(itemToDisplay);
});
// Process the details and the popup will be shown with the limited set of data returned.
process(trademarks);
});
},
updater: function (itemToDisplay) {
// The user selectes a value using the mouse, now get the trademark id by the selected text.
var selectedTrademarkId = parseInt(trademarksHashMap[itemToDisplay].value);
$("#hdnTrademarkId").val(selectedTrademarkId);
// Save the last chosen text to prevent searching if the text not changed.
lastTrademarkNameChosen = itemToDisplay;
// return the text to be displayed inside the textbox.
return itemToDisplay;
}
});
});