Angular child component doesnt recognize input changes - javascript

Hello I use parent child communication. At parent I have array which represents graph datas. Demo
My problem is that when i change one item in array i doesn't fire child component appchanges. In demo I created example which doesn't work. When I click update I want to update child component graph.
My child component
import { Component, OnInit, Input, OnChanges } from "#angular/core";
import * as Highcharts from "highcharts";
import { ChartService } from "./chart.service";
#Component({
selector: "hello",
template: `
<highcharts-chart
[Highcharts]="Highcharts"
[options]="options"
[oneToOne]="true"
[update]="updateFromInput"
style="width: calc(100% ); height: calc(100% - 17px); display: block;margin-top:15px;overflow: auto !important;"
>
</highcharts-chart>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent implements OnInit, OnChanges {
innerHeight: any;
innerWidth: any;
#Input() data: any;
Highcharts: typeof Highcharts = Highcharts;
updateFromInput = false;
options: any;
constructor(private chart_service: ChartService) {
this.innerHeight = window.screen.height;
this.innerWidth = window.screen.width;
}
onResize(event) {
this.innerWidth = event.target.innerWidth;
this.update();
}
ngOnInit() {
this.update();
}
ngOnChanges() {
console.log(this.data);
console.log("değişti");
this.update();
}
update() {
var series = this.chart_service.groupBy(
this.data.LINE.DATA,
"GRUP",
"line"
);
var categories = this.chart_service.ArrNoDupe(
this.data.LINE.DATA.map(x => x.NAME)
);
let isLegend = series.length > 1 ? true : false;
var size = this.innerWidth / (12 / this.data.SIZE) - 30;
if (this.innerWidth <= 992) {
size = this.innerWidth - 30;
}
this.options = {
title: { enabled: false, text: "" },
credits: { enabled: false },
legend: { enabled: true },
tooltip: { hideDelay: 0, outside: true, shared: true },
plotOptions: {
line: { dataLabels: { enabled: !isLegend }, enableMouseTracking: true }
},
yAxis: { title: { enabled: false } },
xAxis: { categories: categories },
series: series
};
}
}

In your app.component.ts:
filter(id) {
this.reports.filter(x => x.ID == id)[0] = {
ID: 3233.0,
...
}
}
Array.prototype.filter does not mutate the given array, but returns a new array with the filtered properties.
You must reassign this value to make the Angular change detection detect this new array:
filter(id) {
this.reports = this.reports.filter(x => x.ID == id)[0] = {
ID: 3233.0,
...
}
}

It looks like there are three mistakes in your code:
Your #Input() getter/setter are switched
#Input() set data(data: any) {
this._param = data;
}
get data(): any {
return this._param;
}
Your filter actually does not filter anything (at least as far as I see from the data)
Your *ngFor needs to be notified, that your data has changed - for that you could implement the ngOnChanges lifecycle hook and assign the latest value from the SimpleChanges of your reports to the member variable reports

Exactly as #code-gorilla mentioned, it is not working because you are not mutating the reports array so the changes are not detected = your chart is not updated.
After modifying your code so it will mutate the reports I was able to make it working, you can find more details in the demo I attached below.
filter(id) {
const updatedReport = (this.reports.filter(
x => x.CHART_ITEM_ID == id
)[0] = { ... }
});
this.reports[0] = updatedReport;
}
Live demo:
https://stackblitz.com/edit/angular-7yr5qg?file=src%2Fapp%2Fapp.component.ts

In general, OnChanges is fired when the data object source has changed, but not if the data has been mutated. In order for an array to cause OnChanges to be called, it must be updated in an immutable fashion.
In this article example, slice is a immutable function - meaning it does not change the original array, whereas splice is a mutable function- meaning it changes the original array.
So in your example, in order for onChanges to be called, you would need to use an immutable function like map instead of filter.
In your specific case, it looks like you do not really want to use filter, since you just need the first element that matches the predicate. You should really use find instead.
have a separate property of report, then have your function call report = this.reports.find(x => x.CHART_ITEM_ID == id) , and bind report to the child

After helpful answer above I assing firstly index to variable then updated with using index like below code Demo
let index=this.reports.findIndex( x => x.CHART_ITEM_ID == id );
this.reports[index]= ...

Related

How to update object data of parent in child

I am making custom component for dropdown. I have one config object which I am initializing in ngOnInit(), and I am combining the default configs and configs provided by user as an #Input(), But at run time from parent component, If I am making any changes in my config object, it is not updating in ngOnChanges() method of my child.
I tried this:
child component
#Input() config: MyConfig;
#Input() disabled: boolean
ngOnChanges() {
console.log('config', this.config); // this is not
console.log('disabled', this.disabled); // this is detecting
}
parent component html
<button (click)="changeConfig()">Change Config</button>
<app-child [config]="customConfig" [disabled]="newDisabled"></app-child>
parent component ts
newDisabled = false;
customConfig = {
value: 'code',
label: 'name',
floatLabel: 'Select'
};
changeConfig() {
this.customConfig.value = 'name';
this.newDisabled = true;
}
for disbale variable it is working, but for config it is not, Am I doing something wrong? please help
You problem is that you ngOnInit is setting the config variable to a new object. Since the #Input() is called once, this breaks your reference to the original object, and changes will not be detected.
You can fix this by using a setter and getter. I have added a stack blitz to demo this bellow.
Blockquote
parent component
import { ChangeDetectorRef, Component, VERSION } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
newDisabled = false;
customConfig = {
value: 'code',
label: 'name',
floatLabel: 'Select',
};
changeConfig1() {
this.customConfig.value = 'name1';
this.newDisabled = true;
console.log('Change Config 1');
}
changeConfig2() {
this.customConfig.value = 'name2';
this.newDisabled = true;
console.log('Change Config 2');
}
}
child component
import { Component, Input } from '#angular/core';
class MyConfig {}
#Component({
selector: 'hello',
template: `<h1> config: {{config | json}}</h1><h1> disabled: {{disabled}}</h1>`,
styles: [],
})
export class HelloComponent {
private _defaultConfig = {
key: 'default',
};
#Input() disabled: boolean;
private _config: MyConfig;
#Input() config: MyConfig;
set () {
if (!this.config) {
this.config = new MyConfig(); // it is a class
} else {
this._config = {
...this.config,
...this._defaultConfig,
};
}
}
get () {
return this._config;
}
ngOnChanges() {
console.log('config', this.config);
console.log('config', this._config);
console.log('disabled', this.disabled);
}
}
The problem is that the change detection is only triggered if the object customConfig is changed. In you example, only the value property is updated. What you can do is the following in the parent.component.ts:
changeConfig() {
this.customConfig = Object.assign(this.customConfig, { value: 'name'});
this.newDisabled = true;
}
This will create a new config object which contains the updated value property and all the other old properties of the old customConfig.
Input object are compared by reference, so if you want to reflect changes in your child component and trigger ngOnChanges do this:
changeConfig() {
this.customConfig = {...this.customConfig, value: 'name'};;
this.newDisabled = true;
}
And also move your below code from ngOnInit to ngOnChanges, chances are that at the time of initialisation input chagnes may not be available.
if (!this.config) {
this.config = new MyConfig(); // it is a class
} else {
this.config = {
...this._defaultConfig,
...this.config
};
}

Class as function in functional component - Vue

I am writing a simple functional component in vuejs. Currently stuck at a situation where I want to add conditional css class based on the props passed to it.
However the below doesn't work as expected and I am wondering what wrong am I doing here.
<script>
export default {
name: "BasePill",
functional: true,
props: {
variant: String
},
render(createElement, { children, props }) {
const componentData = {
staticClass: "text-sm text-center"
class: function() {
if (props.variant === 'secondary') {
return 'bg-secondary'
}
return 'bg-primary'
}
};
return createElement("span", componentData, children);
},
};
</script>
The class property cannot be a function, it needs to be a string/array/object.
Do this instead:
const componentData = {
staticClass: 'text-sm text-center',
class: props.variant === 'secondary' ? 'bg-secondary' : 'bg-primary',
};

not proper use of react lifecycle

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

Vue.js Changing props

I'm a bit confused about how to change properties inside components, let's say I have the following component:
{
props: {
visible: {
type: Boolean,
default: true
}
},
methods: {
hide() {
this.visible = false;
}
}
}
Although it works, it would give the following warning:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible"
(found in component )
Now I'm wondering what the best way to handle this is, obviously the visible property is passed in when created the component in the DOM: <Foo :visible="false"></Foo>
Referencing the code in your fiddle
Somehow, you should decide on one place for the state to live, not two. I don't know whether it's more appropriate to have it just in the Alert or just in it's parent for your use case, but you should pick one.
How to decide where state lives
Does the parent or any sibling component depend on the state?
Yes: Then it should be in the parent (or in some external state management)
No: Then it's easier to have it in the state of the component itself
Kinda both: See below
In some rare cases, you may want a combination. Perhaps you want to give both parent and child the ability to hide the child. Then you should have state in both parent and child (so you don't have to edit the child's props inside child).
For example, child can be visible if: visible && state_visible, where visible comes from props and reflects a value in the parent's state, and state_visible is from the child's state.
I'm not sure if this is the behavour that you want, but here is a snippet. I would kinda assume you actually want to just call the toggleAlert of the parent component when you click on the child.
var Alert = Vue.component('alert', {
template: `
<div class="alert" v-if="visible && state_visible">
Alert<br>
<span v-on:click="close">Close me</span>
</div>`,
props: {
visible: {
required: true,
type: Boolean,
default: false
}
},
data: function() {
return {
state_visible: true
};
},
methods: {
close() {
console.log('Clock this');
this.state_visible = false;
}
}
});
var demo = new Vue({
el: '#demo',
components: {
'alert': Alert
},
data: {
hasAlerts: false
},
methods: {
toggleAlert() {
this.hasAlerts = !this.hasAlerts
}
}
})
.alert {
background-color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo" v-cloak>
<alert :visible="hasAlerts"></alert>
<span v-on:click="toggleAlert">Toggle alerts</span>
</div>
According to the Vue.js component doc:
When the parent property updates, it will flow down to the child, but not the other way around. So, how do we communicate back to the parent when something happens? This is where Vue’s custom event system comes in.
Use $emit('my-event) from the child to send an event to the parent. Receive the event on the child declaration inside the parent with v-on:my-event (or #my-event).
Working example:
// child
Vue.component('child', {
template: '<div><p>Child</p> <button #click="hide">Hide</button></div>',
methods: {
hide () {
this.$emit('child-hide-event')
}
},
})
// parent
new Vue({
el: '#app',
data: {
childVisible: true
},
methods: {
childHide () {
this.childVisible = false
},
childShow () {
this.childVisible = true
}
}
})
.box {
border: solid 1px grey;
padding: 16px;
}
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app" class="box">
<p>Parent | childVisible: {{ childVisible }}</p>
<button #click="childHide">Hide</button>
<button #click="childShow">Show</button>
<p> </p>
<child #child-hide-event="childHide" v-if="childVisible" class="box"></child>
</div>
If the prop is only useful for this child component, give the child a prop like initialVisible, and a data like mutableVisible, and in the created hook (which is called when the component's data structure is assembled), simply this.mutableVisible = this.initialVisible.
If the prop is shared by other children of the parent component, you'll need to make it the parent's data to make it available for all children. Then in the child, this.$emit('visibleChanged', currentVisible) to notify the parent to change visible. In parent's template, use <ThatChild ... :visibleChanged="setVisible" ...>. Take a look at the guide: https://v2.vuejs.org/v2/guide/components.html
After a read of your latest comments it seems that you are concerned about having the logic to show/hide the alerts on the parent. Therefore I would suggest the following:
parent
# template
<alert :alert-visible="alertVisible"></alert>
# script
data () {
alertVisible: false,
...
},
...
Then on the child alert you would $watch the value of the prop and move all logic into the alert:
child (alert)
# script
data: {
visible: false,
...
},
methods: {
hide () {
this.visible = false
},
show () {
this.visible = true
},
...
},
props: [
'alertVisible',
],
watch: {
alertVisible () {
if (this.alertVisible && !this.visible) this.show()
else if (!this.alertVisible && this.visible) this.hide()
},
...
},
...
To help anybody, I was facing the same issue. I just changed my var that was inside v-model="" from props array to data. Remember the difference between props and data, im my case that was not a problem changing it, you should weight your decision.
E.g.:
<v-dialog v-model="dialog" fullscreen hide-overlay transition="dialog-bottom-transition">
Before:
export default {
data: function () {
return {
any-vars: false
}
},
props: {
dialog: false,
notifications: false,
sound: false,
widgets: false
},
methods: {
open: function () {
var vm = this;
vm.dialog = true;
}
}
}
After:
export default {
data: function () {
return {
dialog: false
}
},
props: {
notifications: false,
sound: false,
widgets: false
},
methods: {
open: function () {
var vm = this;
vm.dialog = true;
}
}
}
Maybe it looks like on hack and violates the concept of a single data source, but its work)
This solution is creating local proxy variable and inherit data from props. Next work with proxy variable.
Vue.component("vote", {
data: function() {
return {
like_: this.like,
dislike_: this.dislike,
}
},
props: {
like: {
type: [String, Number],
default: 0
},
dislike: {
type: [String, Number],
default: 0
},
item: {
type: Object
}
},
template: '<div class="tm-voteing"><span class="tm-vote tm-vote-like" #click="onVote(item, \'like\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{like_}}</span></span><span class="tm-vote tm-vote-dislike" #click="onVote(item, \'dislike\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{dislike_}}</span></span></div>',
methods: {
onVote: function(data, action) {
var $this = this;
// instead of jquery ajax can be axios or vue-resource
$.ajax({
method: "POST",
url: "/api/vote/vote",
data: {id: data.id, action: action},
success: function(response) {
if(response.status === "insert") {
$this[action + "_"] = Number($this[action + "_"]) + 1;
} else {
$this[action + "_"] = Number($this[action + "_"]) - 1;
}
},
error: function(response) {
console.error(response);
}
});
}
}
});
use component and pass props
<vote :like="item.vote_like" :dislike="item.vote_dislike" :item="item"></vote>
I wonder why it is missed by others when the warning has a hint
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible" (found in component )
Try creating a computed property out of the prop received in the child component as
computed: {
isVisible => this.visible
}
And use this computed in your child component as well as to emit the changes to your parent.

How to get data resolved in parent component

I'm using Vue v1.0.28 and vue-resource to call my API and get the resource data. So I have a parent component, called Role, which has a child InputOptions. It has a foreach that iterates over the roles.
The big picture of all this is a list of items that can be selected, so the API can return items that are selected beforehand because the user saved/selected them time ago. The point is I can't fill selectedOptions of InputOptions. How could I get that information from parent component? Is that the way to do it, right?
I pasted here a chunk of my code, to try to show better picture of my problem:
role.vue
<template>
<div class="option-blocks">
<input-options
:options="roles"
:selected-options="selected"
:label-key-name.once="'name'"
:on-update="onUpdate"
v-ref:input-options
></input-options>
</div>
</template>
<script type="text/babel">
import InputOptions from 'components/input-options/default'
import Titles from 'steps/titles'
export default {
title: Titles.role,
components: { InputOptions },
methods: {
onUpdate(newSelectedOptions, oldSelectedOptions) {
this.selected = newSelectedOptions
}
},
data() {
return {
roles: [],
selected: [],
}
},
ready() {
this.$http.get('/ajax/roles').then((response) => {
this.roles = response.body
this.selected = this.roles.filter(role => role.checked)
})
}
}
</script>
InputOptions
<template>
<ul class="option-blocks centered">
<li class="option-block" :class="{ active: isSelected(option) }" v-for="option in options" #click="toggleSelect(option)">
<label>{{ option[labelKeyName] }}</label>
</li>
</ul>
</template>
<script type="text/babel">
import Props from 'components/input-options/mixins/props'
export default {
mixins: [ Props ],
computed: {
isSingleSelection() {
return 1 === this.max
}
},
methods: {
toggleSelect(option) {
//...
},
isSelected(option) {
return this.selectedOptions.includes(option)
}
},
data() {
return {}
},
ready() {
// I can't figure out how to do it
// I guess it's here where I need to get that information,
// resolved in a promise of the parent component
this.$watch('selectedOptions', this.onUpdate)
}
}
</script>
Props
export default {
props: {
options: {
required: true
},
labelKeyName: {
required: true
},
max: {},
min: {},
onUpdate: {
required: true
},
noneOptionLabel: {},
selectedOptions: {
type: Array
default: () => []
}
}
}
EDIT
I'm now getting this warning in the console:
[Vue warn]: Data field "selectedOptions" is already defined as a prop. To provide default value for a prop, use the "default" prop option; if you want to pass prop values to an instantiation call, use the "propsData" option. (found in component: <default-input-options>)
Are you using Vue.js version 2.0.3? If so, there is no ready function as specified in http://vuejs.org/api. You can do it in created hook of the component as follows:
// InputOptions component
// ...
data: function() {
return {
selectedOptions: []
}
},
created: function() {
this.$watch('selectedOptions', this.onUpdate)
}
In your InputOptions component, you have the following code:
this.$watch('selectedOptions', this.onUpdate)
But I am unable to see a onUpdate function defined in methods. Instead, it is defined in the parent component role. Can you insert a console.log("selectedOptions updated") to check if it is getting called as per your expectation? I think Vue.js expects methods to be present in the same component.
Alternatively in the above case, I think you are allowed to do this.$parent.onUpdate inside this.$watch(...) - something I have not tried but might work for you.
EDIT: some more thoughts
You may have few more issues - you are trying to observe an array - selectedOptions which is a risky strategy. Arrays don't change - they are like containers for list of objects. But the individual objects inside will change. Therefore your $watch might not trigger for selectedOptions.
Based on my experience with Vue.js till now, I have observed that array changes are registered when you add or delete an item, but not when you change a single object - something you need to verify on your own.
To work around this behaviour, you may have separate component (input-one-option) for each of your input options, in which it is easier to observe changes.
Finally, I found the bug. I wasn't binding the prop as kebab-case

Categories