I'm working on a project using nuxt.js, I'm injecting a function in the context of the application as recommended in the official documentation
https://nuxtjs.org/guide/plugins/#inject-in-root-amp-context
but when I try to call the function inside a props validation I get an error
/plugins/check-props.js
import Vue from 'vue'
Vue.prototype.$checkProps = function(value, arr) {
return arr.indexOf(value) !== -1
}
in a component vue
export default {
props: {
color: {
type: String,
validator: function (value, context) {
this.$checkProps(value, ['success', 'danger'])
}
}
}
ERROR: Cannot read property '$checkProps' of undefined
Does anyone know how I can access "this" within validation?
thanks in advance!
Props validation is done before the component is initialized, so you won't have access to this as you are extending Vue.prototype.
Form their documentation:
Note that props are validated before a component instance is created, so instance properties (e.g. data, computed, etc) will not be available inside default or validator functions.
In general, if $checkProps is only used for checking the value of these props, I would just use a helper function.
// array.helpers.js
export function containsValue(arr, val) {
return arr.indexOf(value) !== -1
}
// component
import { containsValue } from 'path/to/helpers/array.helpers';
props: {
foo: {
//
validator(value) {
return containsValue(['foo', 'bar'], value);
}
}
}
Update
Based on your comments, if you don't want to import this specific function over and over again, you can just Array.prototype.includes see docs
// component
props: {
color: {
//
validator(value) {
return ['success', 'danger'].includes(value);
}
}
}
From the doc:
props are validated before a component instance is created, so
instance properties (e.g. data, computed, etc) will not be available
inside default or validator functions
If you want to access the nuxt plugins you can always use the window object.This is how I do it to access the i18n library
{
validator: (value: any): boolean => {
return window.$nuxt.$te(value);
}
}
In your case:
{
validator: (value: any): boolean => {
return window.$nuxt.$checkProps(value, ['success', 'danger']);
}
}
In any case you should never prototype in the window object. Let nuxt handle it with a plugin:
path: plugins/check-props.js or .ts
function checkProps(value: any, arr: string[]): boolean {
return arr.indexOf(value) !== -1
}
const $checkProps: Plugin = (_context: Context, inject: Inject) => {
inject('checkProps', checkProps);
};
export default $checkProps;
And then in nuxt config
{
plugins: [{ src: 'plugins/check-props.js', ssr: true }]
}
Related
I try to create dynamic object with properties in Vue component with keys and values, but the the only way I managed to do that is to create const userProperties: any = {} then return it in data(). The problem appears when I try to build in two places I had to add //#ts-ignore because compiler complains about:
Type '{}' is missing the following properties from type 'Function': apply, call, bind, prototype, and 5 more.
Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'Function'.
Full code I have so far for component:
<script lang="ts">
import {usePropertiesDefinitionsStore} from "#/stores/properties-definitions";
import {storeToRefs} from "pinia";
import ax from "#/axios/axios"
import { toRaw } from 'vue';
export default {
props: ['userId'],
data() {
const userProperties: any = {}
const propertiesDefinitionsStore = usePropertiesDefinitionsStore()
propertiesDefinitionsStore.fetchPropertiesDefinitionsList('User')
const { propertiesDefinitionsList } = storeToRefs(propertiesDefinitionsStore)
ax.get(`/api/properties?id=` + this.userId).then(json => {
//#ts-ignore
this.userProperties = {}
if (json.data.results) {
json.data.results.forEach((result: any) => {
//#ts-ignore
this.userProperties[result.key] = result.value;
})
}
})
return { propertiesDefinitionsList, userProperties }
},
methods: {
saveProperties() {
const dataRaw = toRaw(this.userProperties)
const keys = Object.getOwnPropertyNames(dataRaw)
const data: any = []
keys.forEach(key => {
data.push({
'type': 'User',
'key': key,
'value': dataRaw[key]
})
})
ax.post(`/api/properties?id=` + this.userId, data).then(json => {
console.log(json)
})
},
onlyActive(propertiesDefinitionsList: any) {
return propertiesDefinitionsList.filter((i: any) => i.active)
}
}
}
</script>
Is there a better way to do that?
I tried to set dynamic structure on object without actually creating type, so my object is similar to Java's Map but created as Javascript object (keys and values).
Why not use Map? and since you're using typescript (and typing is the point), why not create a class or interface for your user properties? if you do use a class, don't forget to call the constructor (or just use an interface)
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;
}
⋮
}
}
I'm trying to extract a function for use across multiple components, but "this" is undefined and I'm unsure of the best practice approach of how to attach the scope so my function knows what "this" is. Can I just pass it as an argument?
Component:-
import goToEvent from "#/common";
export default {
name: "update",
methods: {
goToEvent
common function:-
let goToEvent = (event, upcoming=false) => {
this.$store.dispatch({
type: 'setEventsDay',
day: event.start_date
})
}
export default goToEvent
When I call goToEvent in my component, I get TypeError: Cannot read property '$store' of undefined. How do I avoid this?
In this situation I recommend to define eventable as a mixin :
const eventable= {
methods: {
goToEvent(event, upcoming=false) {
this.$store.dispatch({
type: 'setEventsDay',
day: event.start_date
})
}
}
}
export default eventable;
in your vue file :
import eventable from "#/eventable";
export default {
name: "update",
mixins:[eventable],
....
second solution :
export an object with the function as nested method then import it and spread it inside the methods option :
export default {
goToEvent(event, upcoming=false){
this.$store.dispatch({
type: 'setEventsDay',
day: event.start_date
})
}
}
then :
import goToEvent from "#/common";
export default {
name: "update",
methods: {
...goToEvent,
otherMethod(){},
}
//....
}
You're tagged with Typescript, so you need to tell TS that this actually has a value (note, I do not know VueJS, am using the generic Event types here, there is likely a more valid and correct type!)
First option, manually tell it what there is -
let goToEvent = (this:Event, event, upcoming=false) => {
Other option - tell it what type it is -
let goToEvent: EventHandler = (event, upcoming=false) => {
Of the two I personally prefer the second style for readability.
There are numerous ways to achieve this, here are some that I like to use in my projects:
Method 1: Mixins
Mixins are great for sharing a bunch of methods across components and also easy to implement, although one big con is that you will not be able to import specific methods that you need. Within the mixin, this follows the rules as in components.
File: #/mixins/eventable
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions([])
goToEvent (event, upcoming = false) {
store.dispatch({
type: 'setEventsDay',
day: event.start_date
})
}
}
}
Usage in component:
import eventable from '#/mixins/eventable'
export default {
name: 'ComponentName',
mixins: [eventable],
methods: {
componentMethod () {
this.goToEvent()
}
}
...
Method 2: Static JavaScript files
In some cases, you might have a collection of helper functions kept in a file and want the ability to import as you need.
In your case, you seem to be using a store actions (assumed from the dispatch), hence I'll be including importing and using the store within the static JS file.
File: #/static/js/eventable.js
import store from 'path_to_store_file'
const goToEvent = () => {
store.dispatch('actionName', payload)
}
export default {
goToEvent
}
Note:
Although this is not entirely necessary, but only by declaring the imported function as a method within the component will it be bound to the component instance. This will allow you to access the function in the HTML portion.
Usage in component:
import { goToEvent } from '#/static/js/eventable.js'
export default {
name: 'ComponentName',
methods: {
// Read note before this code block
goToEvent,
componentMethod () {
// When declared as a method
this.goToEvent()
// When not declared, it can still be accessed in the js portion like this
goToEvent()
}
}
...
I'm sending from the parent component a prop: user. Now in the child component I want to make a copy of it without it changing the prop's value.
I tried doing it like this:
export default defineComponent({
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const user = ref(props.apiUser);
return { user };
}
});
But then if I change a value of the user object it also changes the apiUser prop. I thought maybe using Object.assign would work but then the ref isn't reactive anymore.
In Vue 2.0 I would do it like this:
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
return {
user: {}
}
},
mounted() {
this.user = this.apiUser;
// Now I can use this.user without changing this.apiUser's value.
}
};
Credits to #butttons for the comment that lead to the answer.
const user = reactive({ ...props.apiUser });
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const userCopy = toRef(props, 'apiUser')
}
With the composition API we have the toRef API that allows you to create a copy from any source reactive object. Since the props object is a reactive, you use toRef() and it won't mutate your prop.
This is what you looking for: https://vuejs.org/guide/components/props.html#one-way-data-flow
Create data where you add the prop to
export default {
props: ['apiUser'],
data() {
return {
// user only uses this.apiUser as the initial value;
// it is disconnected from future prop updates.
user: this.apiUser
}
}
}
Or if you use api composition:
import {ref} from "vue";
const props = defineProps(['apiUser']);
const user = ref(props.apiUser);
You also may want to consider using computed methods (see also linked doc section from above) or v-model.
Please note that the marked solution https://stackoverflow.com/a/67820271/2311074 is not working. If you try to update user you will see a readonly error on the console. If you don't need to modify user, you may just use the prop in the first place.
As discussed in comment section, a Vue 2 method that I'm personally fond of in these cases is the following, it will basically make a roundtrip when updating a model.
Parent (apiUser) ->
Child (clone apiUser to user, make changes, emit) ->
Parent (Set changes reactively) ->
Child (Automatically receives changes, and creates new clone)
Parent
<template>
<div class="parent-root"
<child :apiUser="apiUser" #setUserData="setUserData" />
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of child component etc.)
export default {
data() {
apiUser: {
id: 'e134',
age: 27
}
},
methods: {
setUserData(payload) {
this.$set(this.apiUser, 'age', payload);
}
}
}
Child
<template>
<div class="child-root"
{{ apiUser }}
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of components etc.)
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
user: null
},
watch: {
apiUser: {
deep: true,
handler() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
}
}
},
mounted() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
},
methods: {
// Whatever function catching the changes you want to do
setUserData(payload) {
this.$emit('setUserData', this.user);
}
}
}
Apologies for any miss types
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