I've been modifying the Meteor1.3+React Todos app to get the basics down and it's been going well so far. However, i'd like to add another text field so that the user can submit a description of an item (the first field that comes with the Todos app) as well as the cost of that item. I've been trying to add the second input field and copy over the values/pass them through to the tasks.js api but I can't seem to get it to work. I'm aware that this is aesthetically unsettling (hitting enter to input two text fields into a collection) and it may be impossible/is most likely not the correct way to do something like this.
Here's what I'm working with:
App.jsx
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { Tasks } from '../api/tasks.js';
import Task from './Task.jsx';
import AccountsUIWrapper from './AccountsUIWrapper.jsx';
// App component - represents the whole app
//render collection of tasks
class App extends Component {
//componenet contructor that contains initializations for: hideCompleted
constructor(props) {
super(props);
this.state = {
hideCompleted: false,
};
}
//Event handler for when you press enter to input data. Calls Meteor method tasks.insert and sends it the text,
//then clears the text form
handleSubmit(event) {
event.preventDefault();
// Find the task text field via the React ref
const taskText = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
// Find the cost field via the React ref
const costNum = ReactDOM.findDOMNode(this.refs.costInput).value.trim();
//Call the tasks insert method in tasks.js api
Meteor.call('tasks.insert', taskText, costNum);
// Clear task form
ReactDOM.findDOMNode(this.refs.textInput).value = '';
//Clear cost form
ReactDOM.findDOMNode(this.refs.textInput).value = '';
}
//Event handler for hideCompleted checkbox check
toggleHideCompleted() {
this.setState({
hideCompleted: !this.state.hideCompleted,
});
}
//Filters out tasks that have hideCompleted === true
renderTasks() {
let filteredTasks = this.props.tasks;
if (this.state.hideCompleted) {
filteredTasks = filteredTasks.filter(task => !task.checked);
}
return filteredTasks.map((task) => (
<Task key={task._id} task={task} />
));
}
render() {
return (
<div className="container">
<header>
<h1>The Economy</h1> ({this.props.incompleteCount})
<label className="hide-completed">
<input
type="checkbox"
readOnly
checked={this.state.hideCompleted}
onClick={this.toggleHideCompleted.bind(this)}
/>
Hide Completed Tasks
</label>
<AccountsUIWrapper />
{ this.props.currentUser ?
<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
<input
type="text"
ref="textInput"
placeholder="Type to add new tasks"
/>
<input
type="Number"
ref="costInput"
placeholder="Type to add cost"
/>
</form> : ''
}
</header>
<ul>
{this.renderTasks()}
</ul>
</div>
);
}
}
//proptypes - set up the tasks proptype
App.propTypes = {
tasks: PropTypes.array.isRequired,
incompleteCount: PropTypes.number.isRequired,
currentUser: PropTypes.object,
};
//exports createContainer function which queries the tasks collection
export default createContainer(() => {
return {
tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
incompleteCount: Tasks.find({ checked: { $ne: true } }).count(),
currentUser: Meteor.user(),
// currentBalance: Tasks.characters.aggregate([ { $group: { _id: null, total: { $sum: "$cost" } } } ]),
};
}, App);
tasks.js
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
export const Tasks = new Mongo.Collection('tasks');
Meteor.methods({
'tasks.insert'(text, cost) {
//Make sure that text is a String
check(text, String);
//Make sure that cost is a Number
check(cost, Number);
// Make sure the user is logged in before inserting a task
if (! this.userId) {
throw new Meteor.Error('not-authorized');
}
//Actual database insertion
Tasks.insert({
text,
cost,
createdAt: new Date(),
owner: this.userId,
username: Meteor.users.findOne(this.userId).username,
});
},
'tasks.remove'(taskId) {
check(taskId, String);
Tasks.remove(taskId);
},
'tasks.setChecked'(taskId, setChecked) {
check(taskId, String);
check(setChecked, Boolean);
Tasks.update(taskId, { $set: { checked: setChecked } });
},
});
I feel like there's some type of simple answer out there but after a while of searching, I can't seem to find anything that makes sense to me. I haven't dabbled in any React forms packages yet because I thought i'd be able to do this without one. I feel bad about asking about something seemingly so simple but alas here I am. Any recommended reading or methods to look into is greatly appreciated.
I think you forgot to add a button to a form
<input type="submit" value="Submit my form" />
Related
I created a small module as a validator inspired from vee-validate and I would like to use this in conjunction with the composition api.
I have a list of errorMessages that are stored in a reactive array, however when I retrieve this variable in my vue component, despite the error messages being stored in the array accordingly, the variable is not updating in the vue template.
I’m not very savvy with this so I might not be concise with my explanation. The refs in the module seem to be working properly.
Can someone kindly indicate what I might be doing wrong? I'm completely stuck and I don't know how else I can proceed.
Validator.js (Npm module - located in node_modules)
import Vue from 'vue';
import VueCompositionAPI from '#vue/composition-api'
import {ref} from '#vue/composition-api'
Vue.use(VueCompositionAPI)
class Validator {
….
register({fieldName, rules, type}) {
if (!fieldName || rules === null || rules === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
let errorMessages = ref([]);
// define callback for pub-sub
const callback = ({id, messages}) => {
if (fieldId === id) {
errorMessages.value = Object.assign([], messages);
console.log(errorMessages.value); // this contains the value of the error messages.
}
};
return {
errorMessages,
};
}
……
InputField.vue
<template>
<div :style="{'width': fieldWidth}" class="form-group">
<label :for="fieldName">
<input
ref="inputField"
:type="type"
:id="fieldName"
:name="fieldName"
:class="[{'field-error': apiError || errorMessages.length > 0}, {'read-only-input': isReadOnly}]"
#input="handleInput"
v-model="input"
class="form-input"/>
</label>
<div>
<p class="text-error">{{errorMessages}}</p> // Error messages not displaying
</div>
</div>
</template>
<script>
import {ref, watch} from '#vue/composition-api';
import Validator from "validator";
export default {
props: {
fieldTitle: {
required: true
},
fieldName: {
required: true
},
type: {
required: true
},
rules: {
default: 'required'
}
},
setup(props) {
// The error messages are returned in the component but they are not reactive. Therefore they only appear after its re-rendered.
const {errorMessages, handleInput, setFieldData} = Validator.register(props);
return {
errorMessages,
handleInput,
}
}
}
</script>
You should be using Vue.Set(), it directly triggers related values to be updated
The problem is Validator.register() directly destructures props, which removes the reactivity from the resulting values.
Solution
Use toRefs(props) to create an object of refs for each prop, and pass that to Validator.register():
import { toRefs } from 'vue'
👇
Validator.register(toRefs(props))
Then update Validator.register() to unwrap the refs where needed:
class Validator {
register({ fieldName, rules, type }) {
👇 👇 👇
if (!fieldName.value || rules.value === null || rules.value === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
⋮
}
}
Problem:
Currently, I have a LoginForm component that has an "on-success" handler function handleOnSuccess. This the then linked to the parent component with an onTokenUpdate property defined by a "token-update" handler function handleUpdateToken. The problem is that the setState in the handleUpdateToken function is forcing an undesired rerender.
Desired Outcome:
What I ultimately need is to update the LoginForm component property token with the value obtained on success WITHOUT performing a rerender. Is this even possible? According to React: Update Child Component Without Rerendering Parent it would seem it is not, however, no feasible alternative for my case was suggested. Im wondering if anyone had any suggested alternatives if this is not possible.
Code
LoginForm.react.js:
import React, { Component } from 'react';
import Script from 'react-load-script';
import PropTypes from 'prop-types';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
linkLoaded: false,
initializeURL: 'https://cdn.plaid.com/link/v2/stable/link-initialize.js',
};
this.onScriptError = this.onScriptError.bind(this);
this.onScriptLoaded = this.onScriptLoaded.bind(this);
this.handleLinkOnLoad = this.handleLinkOnLoad.bind(this);
this.handleOnExit = this.handleOnExit.bind(this);
this.handleOnEvent = this.handleOnEvent.bind(this);
this.handleOnSuccess = this.handleOnSuccess.bind(this);
this.renderWindow = this.renderWindow.bind(this);
}
onScriptError() {
console.error('There was an issue loading the link-initialize.js script');
}
onScriptLoaded() {
window.linkHandler = window.Plaid.create({
apiVersion: this.props.apiVersion,
clientName: this.props.clientName,
env: this.props.env,
key: this.props.publicKey,
onExit: this.handleOnExit,
onLoad: this.handleLinkOnLoad,
onEvent: this.handleOnEvent,
onSuccess: this.handleOnSuccess,
product: this.props.product,
selectAccount: this.props.selectAccount,
token: this.props.token,
webhook: this.props.webhook,
});
console.log("Script loaded");
}
handleLinkOnLoad() {
console.log("loaded");
this.setState({ linkLoaded: true });
}
handleOnSuccess(token, metadata) {
console.log(token);
console.log(metadata);
this.props.onTokenUpdate(token);
}
handleOnExit(error, metadata) {
console.log('link: user exited');
console.log(error, metadata);
}
handleOnLoad() {
console.log('link: loaded');
}
handleOnEvent(eventname, metadata) {
console.log('link: user event', eventname, metadata);
}
renderWindow() {
const institution = this.props.institution || null;
if (window.linkHandler) {
window.linkHandler.open(institution);
}
}
static exit(configurationObject) {
if (window.linkHandler) {
window.linkHandler.exit(configurationObject);
}
}
render() {
return (
<div id={this.props.id}>
{this.renderWindow()}
<Script
url={this.state.initializeURL}
onError={this.onScriptError}
onLoad={this.onScriptLoaded}
/>
</div>
);
}
}
LoginForm.defaultProps = {
apiVersion: 'v2',
env: 'sandbox',
institution: null,
selectAccount: false,
style: {
padding: '6px 4px',
outline: 'none',
background: '#FFFFFF',
border: '2px solid #F1F1F1',
borderRadius: '4px',
},
};
LoginForm.propTypes = {
// id
id: PropTypes.string,
// ApiVersion flag to use new version of Plaid API
apiVersion: PropTypes.string,
// Displayed once a user has successfully linked their account
clientName: PropTypes.string.isRequired,
// The Plaid API environment on which to create user accounts.
// For development and testing, use tartan. For production, use production
env: PropTypes.oneOf(['tartan', 'sandbox', 'development', 'production']).isRequired,
// Open link to a specific institution, for a more custom solution
institution: PropTypes.string,
// The public_key associated with your account; available from
// the Plaid dashboard (https://dashboard.plaid.com)
publicKey: PropTypes.string.isRequired,
// The Plaid products you wish to use, an array containing some of connect,
// auth, identity, income, transactions, assets
product: PropTypes.arrayOf(
PropTypes.oneOf([
// legacy product names
'connect',
'info',
// normal product names
'auth',
'identity',
'income',
'transactions',
'assets',
])
).isRequired,
// Specify an existing user's public token to launch Link in update mode.
// This will cause Link to open directly to the authentication step for
// that user's institution.
token: PropTypes.string,
access_token: PropTypes.string,
// Set to true to launch Link with the 'Select Account' pane enabled.
// Allows users to select an individual account once they've authenticated
selectAccount: PropTypes.bool,
// Specify a webhook to associate with a user.
webhook: PropTypes.string,
// A function that is called when a user has successfully onboarded their
// account. The function should expect two arguments, the public_key and a
// metadata object
onSuccess: PropTypes.func,
// A function that is called when a user has specifically exited Link flow
onExit: PropTypes.func,
// A function that is called when the Link module has finished loading.
// Calls to plaidLinkHandler.open() prior to the onLoad callback will be
// delayed until the module is fully loaded.
onLoad: PropTypes.func,
// A function that is called during a user's flow in Link.
// See
onEvent: PropTypes.func,
onTokenUpdate: PropTypes.func,
// Button Styles as an Object
style: PropTypes.object,
// Button Class names as a String
className: PropTypes.string,
};
export default LoginForm;
App.js:
// /* eslint no-magic-numbers: 0 */
import React, { Component } from 'react';
import { LoginForm } from '../lib';
class App extends Component {
constructor(props) {
super(props);
this.state = {
access_token: null
};
this.handleUpdateToken = this.handleUpdateToken.bind(this)
}
handleUpdateToken(access_token) {
this.setState({ access_token: access_token });
}
render() {
return (
<LoginForm
id="Test"
clientName="Plaid Client"
env="sandbox"
product={['auth', 'transactions']}
publicKey="7a3daf1db208b7d1fe65850572eeb1"
className="some-class-name"
apiVersion="v2"
onTokenUpdate={this.handleUpdateToken}
token={this.state.access_token}
>
</LoginForm>
);
}
}
export default App;
Thanks in advance for any/all help!
You cannot prevent rendering of parent if you want to update the props of child. But there is a way to achieve it.
You need to store the token in side your LoginForm state.
You need to change change the state of the LoginForm in componentWillReceiveProps
You need to pass a token and function in this.props.onTokenUpdate(token,function);.This function will have an argument which is token from parent. And inside function you will change the state.(This is needed if you want to alter the token in parent component and send updated one).
The token in parent shouldn't be in the state if you want to prevent render(). It should be compoent property
Use this.state.tokenInstead of this.props.token
Below is a general example.
Codepen
This is what I ended up doing (but I accepted #MaheerAli answer since some people may have been looking for that instead):
LoginForm.js:
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
linkLoaded: false,
initializeURL: 'https://cdn.plaid.com/link/v2/stable/link-initialize.js',
};
this.onScriptError = this.onScriptError.bind(this);
this.onScriptLoaded = this.onScriptLoaded.bind(this);
this.handleLinkOnLoad = this.handleLinkOnLoad.bind(this);
this.handleOnExit = this.handleOnExit.bind(this);
this.handleOnEvent = this.handleOnEvent.bind(this);
this.handleOnSuccess = this.handleOnSuccess.bind(this);
this.renderWindow = this.renderWindow.bind(this);
}
onScriptError() {
console.error('There was an issue loading the link-initialize.js script');
}
onScriptLoaded() {
window.linkHandler = window.Plaid.create({
apiVersion: this.props.apiVersion,
clientName: this.props.clientName,
env: this.props.env,
key: this.props.publicKey,
onExit: this.handleOnExit,
onLoad: this.handleLinkOnLoad,
onEvent: this.handleOnEvent,
onSuccess: this.handleOnSuccess,
product: this.props.product,
selectAccount: this.props.selectAccount,
token: this.props.token,
webhook: this.props.webhook,
});
}
handleLinkOnLoad() {
console.log("loaded");
this.setState({ linkLoaded: true });
}
handleOnSuccess(token, metadata) {
console.log(token);
console.log(metadata);
this.props.onTokenUpdate(token);
}
handleOnExit(error, metadata) {
console.log('PlaidLink: user exited');
console.log(error, metadata);
}
handleOnLoad() {
console.log('PlaidLink: loaded');
}
handleOnEvent(eventname, metadata) {
console.log('PlaidLink: user event', eventname, metadata);
}
renderWindow() {
const institution = this.props.institution || null;
if (window.linkHandler) {
window.linkHandler.open(institution);
}
}
chooseRender() {
if (this.props.access_token === null) {
this.renderWindow()
}
}
static exit(configurationObject) {
if (window.linkHandler) {
window.linkHandler.exit(configurationObject);
}
}
render() {
return (
<div id={this.props.id}
access_token={this.props.access_token}>
{this.chooseRender()}
<Script
url={this.state.initializeURL}
onError={this.onScriptError}
onLoad={this.onScriptLoaded}
/>
</div>
);
}
}
App.js:
// /* eslint no-magic-numbers: 0 */
import React, { Component } from 'react';
import { LoginForm } from '../lib';
class App extends Component {
constructor(props) {
super(props);
this.state = {
access_token: null
};
this.handleUpdateToken = this.handleUpdateToken.bind(this)
}
handleUpdateToken(access_token) {
this.setState({ access_token: access_token });
}
render() {
return (
<LoginForm
id="Test"
access_token={this.state.access_token}
clientName="Plaid Client"
env="sandbox"
product={['auth', 'transactions']}
publicKey="7a3daf1db208b7d1fe65850572eeb1"
className="some-class-name"
apiVersion="v2"
onTokenUpdate={this.handleUpdateToken}
>
</LoginForm>
);
}
}
export default App;
Most notably, I just defined a chooseRender function which chose whether or not to render Plaid in the Child.
I have a div with a className of results and I want to make so that when I click on the Check button the div gets populated with the info from my store: Shop1, Shop2. I added an observable field on my CardCheck component that you toggle with the onClick event handler, and when the field is true it should display all entries in the auto array.
Here is my Component:
import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';
import PropTypes from 'prop-types';
import './cardCheck.css';
#inject('auto')
#observer
class CardCheck extends Component {
onClick = event => {
event.preventDefault();
this.checked = !this.checked;
};
render() {
const { auto } = this.props;
return (
<div>
<div className="newsletter-container">
<h1>Enter the ID of your card:</h1>
<div className="center">
<input type="number" />
<input type="submit" value="Check" onClick={this.onClick} />
</div>
<div className="results">{this.checked && auto.auto.map(a => <div key={a.name} />)}</div>
</div>
<h1>Offers:</h1>
</div>
);
}
}
CardCheck.propTypes = {
auto: PropTypes.shape({
carCount: PropTypes.string
})
};
export default CardCheck;
and here is my store:
import { observable, action, computed } from 'mobx';
class Auto {
#observable
auto = [
{
name: 'Shop1'
},
{
name: 'Shop2'
}
];
#observable checked = false;
#action showInfo
}
}
export { Auto };
Right now nothing happens when I click on the Check button, why, and how can I make it finally work?
I tried iterating through the object with a loadash method but it did not populated the div, and I also tried this in the div results:
{this.checked &&
auto.auto.map((a, index) => <div key={index}>{a.name}</div>)}
but it didn't work and it gave me this error: Do not use Array index in keys react/no-array-index-key
I think you have mistyped with checked field. It should be this.auto.checked
I have a Sharepoint Framework webpart which basically has a property side bar where I can select the Sharepoint List, and based on the selection it will render the list items from that list into an Office UI DetailsList Component.
When I debug the REST calls are all fine, however the problem is I never get any data rendered on the screen.
so If I select GenericList it should query Generic LIst, if I select Directory it should query the Directory list, however when I select Directory it still says that the selection is GenericList, not directory.
This is my webpart code
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "#microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
IPropertyPaneField,
PropertyPaneLabel
} from "#microsoft/sp-webpart-base";
import * as strings from "FactoryMethodWebPartStrings";
import FactoryMethod from "./components/FactoryMethod";
import { IFactoryMethodProps } from "./components/IFactoryMethodProps";
import { IFactoryMethodWebPartProps } from "./IFactoryMethodWebPartProps";
import * as lodash from "#microsoft/sp-lodash-subset";
import List from "./components/models/List";
import { Environment, EnvironmentType } from "#microsoft/sp-core-library";
import IDataProvider from "./components/dataproviders/IDataProvider";
import MockDataProvider from "./test/MockDataProvider";
import SharePointDataProvider from "./components/dataproviders/SharepointDataProvider";
export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> {
private _dropdownOptions: IPropertyPaneDropdownOption[];
private _selectedList: List;
private _disableDropdown: boolean;
private _dataProvider: IDataProvider;
private _factorymethodContainerComponent: FactoryMethod;
protected onInit(): Promise<void> {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
/*
Create the appropriate data provider depending on where the web part is running.
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the
solution for distribution, that is, using the --ship flag with the package-solution gulp command.
*/
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockDataProvider();
} else {
this._dataProvider = new SharePointDataProvider();
this._dataProvider.webPartContext = this.context;
}
this.openPropertyPane = this.openPropertyPane.bind(this);
/*
Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
*/
this.loadLists()
.then(() => {
/*
If a list is already selected, then we would have stored the list Id in the associated web part property.
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
in the property pane dropdown field.
*/
if (this.properties.spListIndex) {
this.setSelectedList(this.properties.spListIndex.toString());
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
}
});
return super.onInit();
}
// render method of the webpart, actually calls Component
public render(): void {
const element: React.ReactElement<IFactoryMethodProps > = React.createElement(
FactoryMethod,
{
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title,
dataProvider: this._dataProvider,
configureStartCallback: this.openPropertyPane
}
);
// reactDom.render(element, this.domElement);
this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement);
}
// loads lists from the site and fill the dropdown.
private loadLists(): Promise<any> {
return this._dataProvider.getLists()
.then((lists: List[]) => {
// disable dropdown field if there are no results from the server.
this._disableDropdown = lists.length === 0;
if (lists.length !== 0) {
this._dropdownOptions = lists.map((list: List) => {
return {
key: list.Id,
text: list.Title
};
});
}
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
/*
Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
as the selected list for the web part.
*/
if (propertyPath === "spListIndex") {
this.setSelectedList(newValue);
}
/*
Finally, tell property pane to re-render the web part.
This is valid for reactive property pane.
*/
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
// sets the selected list based on the selection from the dropdownlist
private setSelectedList(value: string): void {
const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
(item: IPropertyPaneDropdownOption) => item.key === value
);
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
if (selectedDropDownOption) {
this._selectedList = {
Title: selectedDropDownOption.text,
Id: selectedDropDownOption.key.toString()
};
this._dataProvider.selectedList = this._selectedList;
}
}
// we add fields dynamically to the property pane, in this case its only the list field which we will render
private getGroupFields(): IPropertyPaneField<any>[] {
const fields: IPropertyPaneField<any>[] = [];
// we add the options from the dropdownoptions variable that was populated during init to the dropdown here.
fields.push(PropertyPaneDropdown("spListIndex", {
label: "Select a list",
disabled: this._disableDropdown,
options: this._dropdownOptions
}));
/*
When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
we also add a label field displaying the appropriate message.
*/
if (this._disableDropdown) {
fields.push(PropertyPaneLabel(null, {
text: "Could not find tasks lists in your site. Create one or more tasks list and then try using the web part."
}));
}
return fields;
}
private openPropertyPane(): void {
this.context.propertyPane.open();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
/*
Instead of creating the fields here, we call a method that will return the set of property fields to render.
*/
groupFields: this.getGroupFields()
}
]
}
]
};
}
}
This is my component code
//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import { IFactoryMethodProps } from "./IFactoryMethodProps";
import {
IDetailsListItemState,
IDetailsNewsListItemState,
IDetailsDirectoryListItemState,
IDetailsAnnouncementListItemState,
IFactoryMethodState
} from "./IFactoryMethodState";
import { IListItem } from "./models/IListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { escape } from "#microsoft/sp-lodash-subset";
import { SPHttpClient, SPHttpClientResponse } from "#microsoft/sp-http";
import { ListItemFactory} from "./ListItemFactory";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
buildColumns,
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
import { MarqueeSelection } from "office-ui-fabric-react/lib/MarqueeSelection";
import { autobind } from "office-ui-fabric-react/lib/Utilities";
import PropTypes from "prop-types";
//#endregion
export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> {
constructor(props: IFactoryMethodProps, state: any) {
super(props);
this.setInitialState();
}
// lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
//#region Mouting events lifecycle
// the data returned from render is neither a string nor a DOM node.
// it's a lightweight description of what the DOM should look like.
// inspects this.state and this.props and create the markup.
// when your data changes, the render method is called again.
// react diff the return value from the previous call to render with
// the new one, and generate a minimal set of changes to be applied to the DOM.
public render(): React.ReactElement<IFactoryMethodProps> {
if (this.state.hasError) {
// you can render any custom fallback UI
return <h1>Something went wrong.</h1>;
} else {
switch(this.props.listName) {
case "GenericList":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsListItemState.items} columns={this.state.columns} />;
case "News":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsNewsListItemState.items} columns={this.state.columns}/>;
case "Announcements":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsAnnouncementListItemState.items} columns={this.state.columns}/>;
case "Directory":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsDirectoryListItemState.items} columns={this.state.columns}/>;
default:
return null;
}
}
}
public componentDidCatch(error: any, info: any): void {
// display fallback UI
this.setState({ hasError: true });
// you can also log the error to an error reporting service
console.log(error);
console.log(info);
}
// componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here.
// if you need to load data from a remote endpoint, this is a good place to instantiate the network request.
// this method is a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe in componentWillUnmount().
// calling setState() in this method will trigger an extra rendering, but it is guaranteed to flush during the same tick.
// this guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state.
// use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and
// tooltips when you need to measure a DOM node before rendering something that depends on its size or position.
public componentDidMount(): void {
this._configureWebPart = this._configureWebPart.bind(this);
this.readItemsAndSetStatus();
}
//#endregion
//#region Props changes lifecycle events (after a property changes from parent component)
// componentWillReceiveProps() is invoked before a mounted component receives new props.
// if you need to update the state in response to prop
// changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions
// using this.setState() in this method.
// note that React may call this method even if the props have not changed, so make sure to compare the current
// and next values if you only want to handle changes.
// this may occur when the parent component causes your component to re-render.
// react doesn’t call componentWillReceiveProps() with initial props during mounting. It only calls this
// method if some of component’s props may update
// calling this.setState() generally doesn’t trigger componentWillReceiveProps()
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
if(nextProps.listName !== this.props.listName) {
this.readItemsAndSetStatus();
}
}
//#endregion
//#region private methods
private _configureWebPart(): void {
this.props.configureStartCallback();
}
public setInitialState(): void {
this.state = {
hasError: false,
status: this.listNotConfigured(this.props)
? "Please configure list in Web Part properties"
: "Ready",
columns:[],
DetailsListItemState:{
items:[]
},
DetailsNewsListItemState:{
items:[]
},
DetailsDirectoryListItemState:{
items:[]
},
DetailsAnnouncementListItemState:{
items:[]
},
};
}
// reusable inline component
private ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
<div>
<DetailsList
items={ itemState.items }
columns={ itemState.columns }
setKey="set"
layoutMode={ DetailsListLayoutMode.fixedColumns }
selectionPreservedOnEmptyClick={ true }
compact={ true }>
</DetailsList>
</div>
)
// read items using factory method pattern and sets state accordingly
private readItemsAndSetStatus(): void {
this.setState({
status: "Loading all items..."
});
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName)
.then((items: any[]) => {
var myItems: any = null;
switch(this.props.listName) {
case "GenericList":
myItems = items as IListItem[];
break;
case "News":
myItems = items as INewsListItem[];
break;
case "Announcements":
myItems = items as IAnnouncementListItem[];
break;
case "Directory":
myItems = items as IDirectoryListItem[];
break;
}
const keyPart: string = this.props.listName === "GenericList" ? "" : this.props.listName;
// the explicit specification of the type argument `keyof {}` is bad and
// it should not be required.
this.setState<keyof {}>({
status: `Successfully loaded ${items.length} items`,
["Details" + keyPart + "ListItemState"] : {
myItems
},
columns: buildColumns(myItems)
});
});
}
private listNotConfigured(props: IFactoryMethodProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
//#endregion
}
I think the rest of the code is not neccesary
Update
SharepointDataProvider.ts
import {
SPHttpClient,
SPHttpClientBatch,
SPHttpClientResponse
} from "#microsoft/sp-http";
import { IWebPartContext } from "#microsoft/sp-webpart-base";
import List from "../models/List";
import IDataProvider from "./IDataProvider";
export default class SharePointDataProvider implements IDataProvider {
private _selectedList: List;
private _lists: List[];
private _listsUrl: string;
private _listItemsUrl: string;
private _webPartContext: IWebPartContext;
public set selectedList(value: List) {
this._selectedList = value;
this._listItemsUrl = `${this._listsUrl}(guid'${value.Id}')/items`;
}
public get selectedList(): List {
return this._selectedList;
}
public set webPartContext(value: IWebPartContext) {
this._webPartContext = value;
this._listsUrl = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists`;
}
public get webPartContext(): IWebPartContext {
return this._webPartContext;
}
// get all lists, not only tasks lists
public getLists(): Promise<List[]> {
// const listTemplateId: string = '171';
// const queryString: string = `?$filter=BaseTemplate eq ${listTemplateId}`;
// const queryUrl: string = this._listsUrl + queryString;
return this._webPartContext.spHttpClient.get(this._listsUrl, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
})
.then((json: { value: List[] }) => {
return this._lists = json.value;
});
}
}
Idataprovider.ts
import { IWebPartContext } from "#microsoft/sp-webpart-base";
import List from "../models/List";
import {IListItem} from "../models/IListItem";
interface IDataProvider {
selectedList: List;
webPartContext: IWebPartContext;
getLists(): Promise<List[]>;
}
export default IDataProvider;
When the list name changes, you're invoking readItemsAndSetStatus:
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
if(nextProps.listName !== this.props.listName) {
this.readItemsAndSetStatus();
}
}
However, readItemsAndSetStatus doesn't take a parameter, and continues to use this.props.listName, which hasn't changed yet.
private readItemsAndSetStatus(): void {
...
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName)
...
}
Try passing nextProps.listName to readItemsAndSetStatus:
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
if(nextProps.listName !== this.props.listName) {
this.readItemsAndSetStatus(nextProps.listName);
}
}
Then either use the incoming parameter, or default to this.props.listName:
private readItemsAndSetStatus(listName): void {
...
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(this.props.spHttpClient, this.props.siteUrl, listName || this.props.listName)
...
}
In your first "webpart code", the onInit() method returns before loadLists() finishes:
onInit() {
this.loadLists() // <-- Sets this._dropdownOptions
.then(() => {
this.setSelectedList();
});
return super.onInit(); // <-- Doesn't wait for the promise to resolve
}
This means that getGroupFields() might not have data for _dropdownOptions. That means that getPropertyPaneConfiguration() might not have the right data.
I'm not positive that's the problem, or the only problem. I don't have any experience with SharePoint, so take all of this with a grain of salt.
I see that in the react-todo-basic they are doing the same thing you are.
However, elsewhere I see people performing additional actions within the super.onInit Promise:
react-list-form
react-sp-pnp-js-property-decorator
I'm trying to get a value in an object of javascript but it fails somehow. I managed to get an intended data from mongoDB by findOne method. Here is my code and console log.
const title = Questions.findOne({_id: props.match.params.id});
console.log(title);
Then console says:
Object {_id: "bpMgRnZxh5L4rQjP9", text: "Do you like apple?"}
What I wanna get is only the text in the object. I have already tried these.
console.log(title.text);
console.log(title[text]);
console.log(title["text"]);
console.log(title[0].text);
But I couldn't access to it... The error message is below.
Uncaught TypeError: Cannot read property 'text' of undefined
It sounds super easy but I couldn't solve by my self. Could anyone help me out?
Additional Context
I'm using Meteor and React. I would like to pass the text inside of the object from the container to the class. I would like to render the text in render(). But it doesn't receive any data from the container... The console.log in the container works well and shows the object.
import React, { Component } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import { Questions } from '../../api/questions.js';
import PropTypes from 'prop-types';
import { Answers } from '../../api/answers.js';
import ReactDOM from 'react-dom';
import { Chart } from 'react-google-charts';
class MapClass extends React.Component{
handleAlternate(event){
event.preventDefault();
const country = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
Answers.insert({
country,
yes: false,
question_id:this.props.match._id,
createdAt: new Date(), // current time
});
ReactDOM.findDOMNode(this.refs.textInput).value = '';
}
handleSubmit(event) {
event.preventDefault();
const country = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
Answers.insert({
country,
yes: true,
question_id: this.props.match.params.id,
createdAt: new Date(), // current time
});
ReactDOM.findDOMNode(this.refs.textInput).value = '';
}
constructor(props) {
super(props);
this.state = {
options: {
title: 'Age vs. Weight comparison',
},
data: [
['Country', 'Popularity'],
['South America', 12],
['Canada', 5.5],
['France', 14],
['Russia', 5],
['Australia', 3.5],
],
};
this.state.data.push(['China', 40]);
}
render() {
return (
<div>
<h1>{this.props.title.text}</h1>
<Chart
chartType="GeoChart"
data={this.state.data}
options={this.state.options}
graph_id="ScatterChart"
width="900px"
height="400px"
legend_toggle
/>
<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
<input
type="text"
ref="textInput"
placeholder="Type to add new tasks"
/>
<button type="submit">Yes</button>
<button onClick={this.handleAlternate.bind(this)}>No</button>
</form>
</div>
);
}
}
export default MapContainer = createContainer(props => {
console.log(Questions.findOne({_id: props.match.params.id}));
return {
title: Questions.findOne({_id: props.match.params.id})
};
}, MapClass);
The problem here is that at the moment when the container is mounted, the data is not yet available. Since I do not see any subscriptions in your container I assume that you handle that elsewhere and thus there is no way of knowing when the data is ready. You have 2 options.
1) move the subscription into the container and use the subscription handle ready() function to assess if the data is ready. Show a spinner or something while it is not. Read this.
2) use lodash/get function (docs) to handle empty props. You would need to
npm install --save lodash
and then
import get from 'lodash/get';
and then in your class render method:
render() {
const text = get(this.props, 'title.text', 'you-can-even-define-a-default-value-here');
// then use `text` in your h1.
return (...);
}
Does this work for you?