I am exploring TS and Angular. I wanted to update my parent component array with and EventEmitter call. I did it so far but then noticed I don't need to cause my parent array is connected to my component variables (I guess so, that why I ask).
I generate my child component dynamically and initialize my child objects from the parent array. Is my template-task task$ Object so a reference to to the $task object from my parent array ?
Parent component:
<div *ngIf="task" class="elements">
<app-template-task (ping)="update($event)" [task$]="task" *ngFor="let task of $tasks"></app-template-task>
</div>
Child component HTML:
<div class="checkbox">
<img *ngIf="task$.done" (click)="changeStatus($event)" src="../../../assets/checked.png" alt="">
<img *ngIf="!task$.done" (click)="changeStatus($event)" src="../../../assets/unchecked.png" alt="">
</div>
<div class="text">
<input type="text" [(ngModel)]="task$.todoText" (blur)="changeText($event)" placeholder="Name des Todo punkts">
</div>
TS from parent:
public task: boolean;
public taskDone: boolean;
public $tasks: Todo[];
public $tasksdone: Todo[];
constructor() {
this.task = true;
this.taskDone = true;
this.$tasks = [
{
id: 0,
todoText: "Task 1",
done: false,
position: 1
},
{
id: 1,
todoText: "Task 2",
done: false,
position: 2
}
]
this.$tasksdone = []
}
TS from Child:
#Input() task$!: Todo; //! erlaubt es das theoretisch task auch null sein darf
#Output() ping: EventEmitter<any> = new EventEmitter<any>();
constructor() {
}
public changeStatus(event? : any): void{
this.task$.done = !this.task$.done
this.sendEvent("changed")
}
public changeText(event? : any): void{
console.log(this.task$.todoText)
this.sendEvent("textChanged")
}
private sendEvent(eventIdentifier: string): void{
const eventObject: Eventping = {
label: eventIdentifier,
object: this.task$
}
this.ping.emit(eventObject)
}
Yes, you're passing a reference up there: [task$]="task"
Having references in data bindings is good. Mutating objects in child components on the other hand is a bad thing to do.
So you should only use your data from an #Input, not change it.
The only place to change data is the parent component (where it's originally located).
I would recommend you making your child component a "dumb" one (means that it only shows data and tells parent what's going on, and the parent changes the data accordingly).
child.component.html :
<div class="checkbox">
<img *ngIf="task.done" (click)="changeStatus()" src="../../../assets/checked.png" alt="">
<img *ngIf="!task.done" (click)="changeStatus()" src="../../../assets/unchecked.png" alt="">
</div>
<div class="text">
<input type="text" [value]="task.todoText" (change)="changeText($event)" placeholder="Name des Todo punkts">
</div>
child.component.ts :
#Input() task: Todo;
// you can check docs on Partial here: https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype
#Output() ping: EventEmitter<any> = new EventEmitter<Partial<Todo>>();
constructor(){}
public changeStatus(): void {
this.ping.next({ done: !this.task$.done });
}
public changeText(newText: string): void {
this.ping.next({ todoText: newText });
}
Then your parent component should handle the updates and finally change your precious data :)
parent.component.html :
<div *ngIf="task" class="elements">
<app-template-task (ping)="update(idx, $event)" [task]="task" *ngFor="let task of $tasks; let idx = index"></app-template-task>
</div>
parent.component.ts :
public task = true;
public taskDone = true;
public tasks: Todo[] = [
{
id: 0,
todoText: "Task 1",
done: false,
position: 1
},
{
id: 1,
todoText: "Task 2",
done: false,
position: 2
}
];
public tasksDone: Todo[] = [];
constructor(){
}
update(taskIdx: number, changes: Partial<Todo>) {
// here we update reference by creating a new array with the .map method
// it's needed for the Angular Change Detection worked.
this.tasks = this.tasks.map((task, idx) => {
if (idx === taskIdx) return { ...task, ...changes };
return task;
});
// do anything else if needed
}
Basically that's it. I hope it'll help :)
Related
I'm working on building a set of filters, so I'm just trying to make use of the salesChannels array content in my view, which only gets populated when clicking the button with the test() function. The log in ngOnInit outputs an empty array the first time, but works correctly after pressing the button.
The getOrganisationChannels returns an observable.
What causes this behavior and how do I handle it properly? I tried using an eventEmitter to try and trigger the populating but that doesn't work.
TYPESCRIPT
export class SalesChannelFilterComponent implements OnInit {
constructor(
public organizationService: OrganizationService
) { }
#Input() organizationId: any;
salesChannels: Array<any> = [];
selectedChannels: Array<any> = [];
allSelected: Array<any> = [];
ngOnInit() {
this.getChannels();
console.log(this.salesChannels);
}
getChannels() {
this.organizationService.getOrganizationChannels(this.organizationId).subscribe(
salesChannels => {
this.salesChannels = salesChannels;
})
}
test() {
console.log(this.salesChannels);
}
}
HTML
<div>
{{ salesChannels | json }}
</div>
<button (click)="test()">test</button>
<div *ngFor="let channel of salesChannels; let i = index;" class="checkbox c-checkbox">
<label>
<input type="checkbox">
<span class="fa fa-check"></span>{{channel.name}}
</label>
</div>
This is expected behaviour since you are populating the salesChannel in the subscription of an Observable. It's recommended that you use aysnc pipe to let angular check for changes and update the view accordingly.
Component.ts :
export class SalesChannelFilterComponent implements OnInit {
constructor(
public organizationService: OrganizationService
) { }
#Input() organizationId: any;
salesChannels$!: Observable<Array<any>>;
selectedChannels: Array<any> = [];
allSelected: Array<any> = [];
ngOnInit() {
this.getChannels();
console.log(this.salesChannels);
}
getChannels() {
this.salesChannels$ = this.this.organizationService.getOrganizationChannels(this.organizationId);
}
test() {
console.log(this.salesChannels);
}
}
In your template:
<button (click)="test()">test</button>
<div *ngFor="let channel of salesChannels$ | async; let i = index;" class="checkbox c-checkbox">
<label>
<input type="checkbox">
<span class="fa fa-check"></span>{{channel.name}}
</label>
</div>
More details: https://angular.io/api/common/AsyncPipe
I recommend using AsyncPipe here:
<div>{{ salesChannels | async}}</div>
and in .ts:
salesChannels = this.organizationService.getOrganizationChannels(this.organizationId)
I would simply like to delete an item on click, I made a code but I have error, I've been stuck on it for 2 days.
ERROR TypeError: this.addedBook.indexOf is not a function
I have already asked the question on the site we closed it for lack of information yet I am clear and precise
Thank you for your help
service
export class BookService {
url: string = 'http://henri-potier.xebia.fr/books';
public booktype: BookType[];
item: any = [];
constructor(private http: HttpClient) { }
getBookList(): Observable<BookType[]> {
return this.http.get<BookType[]>(this.url);
}
addToBook() {
this.item.push(this.booktype);
}
}
addToBook() here for add book but i dont know how to use it to display added books in my ts file
ts.file
export class PaymentComponent implements OnInit {
addedBook: any = [];
product:any;
constructor(private bookService: BookService) { }
ngOnInit(): void {
this.addedBook = this.bookService.getBookList();
}
delete() {
this.addedBook.splice(this.addedBook.indexOf(this.product), 1);
}
}
html
<div class="product" *ngFor="let book of addedBook | async">
<div class="product-image">
<img [src]="book.cover" alt="book">
</div>
<div class="product-details">
<div class="product-title">{{book.title}}</div>
</div>
<div class="product-price">{{book.price | currency: 'EUR'}}</div>
<div class="product-quantity">
<input type="number" value="1" min="1">
</div>
<div class="product-removal">
<button class="remove-product" (click)="delete()">
Supprimé
</button>
</div>
interface
export interface BookType {
title: string;
price: number;
cover: string;
synopsis: string;
}
I think this.bookService.getBookList() returns Observable so for you case it is not the best solution use async pipe. You should simply subscribe to your server response and than asign it to your variable. and after deleting item only rerender your ngFor.
JS
export class PaymentComponent implements OnInit {
addedBook: any[] = [];
product:any;
constructor(private bookService: BookService) { }
ngOnInit(): void {
// Observable
this.bookService.getBookList().subscribe(response =>{
this.addedBook = response;
});
// Promise
/*
this.bookService.getBookList().then(response=>{
this.addedBook = response;
})*/
}
delete(){
this.addedBook.splice(this.addedBook.indexOf(this.product), 1);
// rerender your array
this.addedBook = [...this.addedBook];
}
}
HTML
<div class="product" *ngFor="let book of addedBook">
<div class="product-image">
<img [src]="book.cover" alt="book">
</div>
<div class="product-details">
<div class="product-title">{{book.title}}</div>
</div>
<div class="product-price">{{book.price | currency: 'EUR'}}</div>
<div class="product-quantity">
<input type="number" value="1" min="1">
</div>
<div class="product-removal">
<button class="remove-product" (click)="delete()">
Supprimé
</button>
</div>
UPDATE
I built a special stackblitz so you can see it in action
here is the link;
you can't use javascript splice on Observable stream, it is not an Array.
to be able to remove an item from a stream you need to combine it (the stream) with another stream (in your case) the id of the item you want to remove.
so first create 2 streams
// the $ sign at the end of the variable name is just an indication that this variable is an observable stream
bookList$: Observable<any[]>; // holds bookList stream
deleteBook$ = new Subject<{ id: string }>(); // holds book id stream
now pass the results you get from your database (which is an observable stream) to bookList$ stream you just created like that
ngOnInit(): void {
this.bookList$ = this.bookService.getBookList().pipe(
delay(0)
);
}
change your html template to that.. and pipe the results from database like that
<div class="product" *ngFor="let book of (bookList$ | sync)">
...
// make sure you include your`remove-product` button inside `*ngFor` loop so you can pass the `book id` you want to remove to the `delete()` function.
<button class="remove-product" (click)="delete(book)">
Supprimé
</button>
</div>
now back to your ts file where we gonna remove the item from the STREAM by modifying the Array and return a new stream.
bookList$: Observable<any[]>; // holds bookList stream
deleteBook$ = new Subject<{ id: string }>(); // holds book id stream
ngOnInit(): void {
this.bookList$ = this.this.bookService.getBookList().pipe(
delay(0)
);
combineLatest([
this.bookList$,
this.deleteBook$
]).pipe(
take1(),
map(([bookList, deleteBook]) => {
if (deleteBook) {
var index = bookList.findIndex((book: any) => book.id === deleteBook.id);
if (index >= 0) {
bookList.splice(index, 1);
}
return bookList;
}
else {
return bookList.concat(deleteBook);
}
})
).subscribe();
}
now all is left to do is remove the item
delete(book: any) {
this.deleteBook$.next({ id: book.id }); pass the book you want to remove to the stream, `combineLatest` will take care of the rest
}
if you make an exit please don't forget me :)
good luck!
From your code, we can see that getBookList() return an Observable. As addedBook is not a array reference it will won't have array methods. That is the cause for your issue.
If you want to do some operations from the service data, subscribe to the observable and store the reference of the value to addedBook.
export class PaymentComponent implements OnInit {
...
ngOnInit(): void {
this.bookService.getBookList().subscribe(
res => { this.addedBook = res }
);
}
...
}
And you need to remove the async keyword from your html
Typescript is mainly used to identify these kind of issues in compile time. The reason it doesn't throw error on compile time is that you've specified addedBook as any. While declaring you declare it as array and onInit you change it to observable, which can be avoided if you've specified type[] ex: string[]
I would suggest something like this
Service file
export class BookService {
url: string = 'http://henri-potier.xebia.fr/books';
//add an observable here
private bookUpdated = new Subject<bookType>();
public booktype: BookType[] = [];//initializa empty array
item: any = [];
constructor(private http: HttpClient) { }
//Ive changet the get method like this
getBookList(){
this.http.get<bookType>(url).subscribe((response) =>{
this.bookType.push(response);//Here you add the server response into the array
//here you can console log to check eg: console.log(this.bookType);
//next you need to use the spread operator
this.bookUpdated.next([...this.bookType]);
});
}
bookUpdateListener() {
return this.bookUpdated.asObservable();//You can subscribe to this in you TS file
}
}
Now in your TS file you should subscribe to the update listener. This is typically done in NgOnInit
Something like this:
export class PaymentComponent implements OnInit {
addedBook: BookType;
product:any;
constructor(private bookService: BookService) { }
ngOnInit(): void {
this.bookService.bookUpdateListener().subscribe((response)=>{
this.addedBook = response;//this will happen every time the service class
//updates the book
});
//Here you can call the get book method
this.bookService.getBookList();
}
delete() {
this.addedBook.splice(this.addedBook.indexOf(this.product), 1);
}
}
Essentially what happens is you are subscribed to when books get changed or updated. Now you can simply use addedBook.title or whatever you want in your HTML.
I have an an Array of Objects like this :
let tree = [
{
task: "Some Task",
spentTime : 2,
subTasks: {
task: "Some Sub Task",
spentTime: 1,
subTasks:{
task:"Some sub sub task",
spentTime:30
}
}
}
]
As you can see here i have this type of tree structure and i am displaying that some kind of nested accordion. So every node has a input box which has 2 way binding with spentTime property ( using v-model ).
Now if i type in any of the node's input. i need to do some operation on these spentTime values and re-populate Or insert different values in the same object.
Here i was thinking doing deep watch. But i think this will cause infinite loop because i am changing the same object and assigning value back and it triggers watch again :)
What i can do if i want to trigger a function on input change and put different values back in the same object.
Thanks!
I had similar reactivity issues with Vue.js
Try to use Vue.set or this.$set to save any changes to your array :
this.$set(this.someObject, 'b', 2)
You can read more about Vue set here.
You can read more information about Vue reactivity here.
tl;dr
The clean solution, based on #djiss suggestion, and which correctly bubbles up to top parent, using $set and watch, is here:
https://codesandbox.io/s/peaceful-kilby-yqy9v
What's below is the initial answer/logic, which uses $emit and the task 'key' to move the update in the parent.
In Vue you can't modify the child directly. I mean, you can, but you shouldn't. When you do it, Vue warns you about it informing you the change you just made will be overridden as soon as the parent changes.
The only options are to use state to manage the single source of trouth for your app (Vuex or a simple Vue object), or you call the parent telling it: "Change this particular child with this particular value". And you simply listen to changes coming from parent.
Which is what I did here:
const task = {
task: "Some Task",
spentTime: 2,
subTasks: [{
task: "Some Sub Task",
spentTime: 1,
subTasks: [{
task: "Some sub sub task",
spentTime: 30
}, {
task: "Some other sub sub task",
spentTime: 12
}]
}]
};
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('Task', {
template: `
<div>
<h2>{{task.task}} ({{spentTime}})</h2>
<div v-if="hasTasks">
<Task v-for="(t, k) in task.subTasks" :key="k" :task="t" #fromChild="fromChild" :tid="k"/>
</div>
<input v-else v-model="localTime" type="number" #input="updateParent(localTime)">
</div>
`,
props: {
task: {
type: Object,
required: true
},
tid: {
type: Number,
default: 0
}
},
data: () => ({
localTime: 0
}),
mounted() {
this.updateParent(this.spentTime);
},
computed: {
spentTime() {
return this.hasTasks ? this.subtasksTotal : this.task.spentTime;
},
subtasksTotal() {
return this.task.subTasks.map(t => t.spentTime).reduce(this.sum, 0)
},
hasTasks() {
return !!(this.task.subTasks && this.task.subTasks.length);
}
},
methods: {
fromChild(time, task) {
this.task.subTasks[task].spentTime = time;
this.updateParent(this.spentTime);
},
updateParent(time) {
this.$emit("fromChild", Number(time), this.tid);
this.localTime = this.spentTime;
},
sum: (a, b) => a + b
},
watch: {
"task.spentTime": function() {
this.localTime = this.task.spentTime;
}
}
});
new Vue({
el: "#app",
data: () => ({
task
}),
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<Task :task="task" :tid="0" />
</div>
It will consume any tree you throw at it, provided it has the same structure. The logic is: show the input if no subtasks or calculate from subtasks otherwise.
Obviously, you can change that to fit your needs.
I ran into this headache before, and I did solve/ cheat on it with deep watch and Lodash _.cloneDeep and _.isEqual.
Inside your child component, create your own data componentTask. You will watch your componentTask and your prop. Every time they change, compare them using _.isEqual. When componentTask changes, emit an event to its parent.
SubTask:
<template>
<div>
<input type="text" v-model="componentTask.task">
<input type="number" min="0" v-model.number="componentTask.spentTime">
<SubTask v-if="task.subTasks" #task-change="handleTaskChange" :task="task.subTasks" />
</div>
</template>
<script lang="ts">
import {Vue, Component, Prop, Watch} from 'vue-property-decorator'
import {Task} from "#/components/Test/Test";
import _ from "lodash";
#Component
export default class SubTask extends Vue {
#Prop() task!: Task;
componentTask: Task | undefined = this.task;
#Watch('task', {deep: true, immediate: true})
onTaskChange(val: Task, oldVal: Task) {
if (_.isEqual(this.componentTask, val))
return;
this.componentTask = _.cloneDeep(val);
}
#Watch('componentTask', {deep: true, immediate: true})
onComponentTaskChange(val: Task, oldVal: Task) {
if (_.isEqual(val, this.task))
return;
this.$emit("task-change");
}
handleTaskChange(subTasks: Task){
this.componentTask = subTasks;
}
}
</script>
Parent class:
<template>
<div style="margin-top: 400px">
<h1>Parent Task</h1>
<br>
<div style="display: flex;">
<div style="width: 200px">
<h4>task</h4>
<p>{{task.task}}</p>
<p>{{task.spentTime}}</p>
<br>
</div>
<div style="width: 200px">
<h4>task.subTasks</h4>
<p>{{task.subTasks.task}}</p>
<p>{{task.subTasks.spentTime}}</p>
<br>
</div>
<div style="width: 200px">
<h4>task.subTasks.subTasks</h4>
<p>{{task.subTasks.subTasks.task}}</p>
<p>{{task.subTasks.subTasks.spentTime}}</p>
<br>
</div>
</div>
<SubTask :task="task" #task-change="handleTaskChange"/>
</div>
</template>
<script lang="ts">
import {Vue, Component, Prop} from 'vue-property-decorator'
import SubTask from "#/components/Test/SubTask.vue";
import {defaultTask, Task} from "#/components/Test/Test";
#Component({
components: {SubTask}
})
export default class Test extends Vue {
task: Task = defaultTask;
handleTaskChange(task: Task) {
this.task = task;
}
}
</script>
Defined interface:
export interface Task {
task: string;
spentTime: number;
subTasks?: Task;
}
export const defaultTask: Task = {
task: "Some Task",
spentTime : 2,
subTasks: {
task: "Some Sub Task",
spentTime: 1,
subTasks:{
task:"Some sub sub task",
spentTime:30
}
}
};
I have created following react class in my code base and trying to use the variable includeDocReqSig in the render method.
Refer to following lines of code in the code below -
console.log(this.includeDocReqSig); //This print the objects pretty fine in the logs but not get assigned in render function
It does not work with following code -
export class NwhERequestForm extends React.Component<INwhERequestFormProps, {}> {
// Dropdown Variables
private includeDocReqSig: IControlDynamicProp = {}; // Dropdown value for Does this include documents that requires signature or legal review?
private eRequestService: ERequestService;
private appSettings: AppSettings;
private serviceCalls: NwhERequestFormRest;
//
public componentWillMount(): Promise<void> {
this.eRequestService = new ERequestService(
this.props.context.pageContext.web.absoluteUrl,
this.props.context.spHttpClient
);
this.appSettings = new AppSettings();
this.serviceCalls = new NwhERequestFormRest(this.eRequestService, this.appSettings);
this.serviceCalls._getFieldChoice("Signature_x0020_Required", true).then(
(val: IControlDynamicProp) => {
this.includeDocReqSig = { options: val.options, disabled: val.disabled };
console.log(this.includeDocReqSig); //This print the objects pretty fine in the logs but not get assigned in render function
});
return Promise.resolve();
}
public render(): React.ReactElement<INwhERequestFormProps> {
return (
<div className={styles.nwhERequestForm} >
<div className={styles.container}>
<div className={styles.title}> {this.props.description} </div>
<div className={styles.subtitle}> If you need assistance, please click here </div>
<form>
<div className={styles.mainformdiv}>
<fieldset className={styles.fieldset}>
<legend className={styles.legend}>Basic Information</legend>
<div className={styles.row}>
<DropdownControl
staticProp={{ labelTitle: 'Does this include a Vendor document that requires signature or requires legal review?', required: true }}
dynamicProp={this.includeDocReqSig} />
<DropdownControl
staticProp={{ labelTitle: 'Is this related to an OCIO Project?', required: true }}
dynamicProp={{ options: signatureRequiredLegal }} />
</div>
</fieldset>
</div>
</form>
</div>
</div>
);
}
}
The function _getFieldChoice is defined in another ts file:
public _getFieldChoice = async (columnName: string, isDisabled: boolean, ) => {
let controlProp: IControlDynamicProp = {};
let dropdownValue: IDropdownOption[] = [];
const fieldChoices: IDropdownValues[] = await this.eRequestService.getFieldDDValue(this.appSettings.eRequestListName, columnName);
fieldChoices[0].Choices.forEach(element => {
dropdownValue.push({ key: element, text: element });
});
controlProp = { options: dropdownValue, disabled: isDisabled };
return controlProp;
}
The dropdown does not get any values. What could be the reason?
When I try to do the assignment outside the function, it works fine. So there is something happening with the scope, maybe??
export class NwhERequestForm extends React.Component<INwhERequestFormProps, {}> {
// Dropdown Variables
private includeDocReqSig: IControlDynamicProp = {}; // Dropdown value for Does this include documents that requires signature or legal review?
private eRequestService: ERequestService;
private appSettings: AppSettings;
private serviceCalls: NwhERequestFormRest;
//
const fundedBy: IDropdownOption[] = [
{ key: 'ocio', text: 'OCIO' },
{ key: 'nonocio', text: 'Non-OCIO' },
{ key: 'split', text: 'Split' },
];
public componentWillMount(): Promise<void> {
this.eRequestService = new ERequestService(
this.props.context.pageContext.web.absoluteUrl,
this.props.context.spHttpClient
);
this.appSettings = new AppSettings();
this.serviceCalls = new NwhERequestFormRest(this.eRequestService, this.appSettings);
this.includeDocReqSig = { options: fundedBy, disabled: false };
return Promise.resolve();
}
public render(): React.ReactElement<INwhERequestFormProps> {
return (
<div className={styles.nwhERequestForm} >
<div className={styles.container}>
<div className={styles.title}> {this.props.description} </div>
<div className={styles.subtitle}> If you need assistance, please click here </div>
<form>
<div className={styles.mainformdiv}>
<fieldset className={styles.fieldset}>
<legend className={styles.legend}>Basic Information</legend>
<div className={styles.row}>
<DropdownControl
staticProp={{ labelTitle: 'Does this include a Vendor document that requires signature or requires legal review?', required: true }}
dynamicProp={**this.includeDocReqSig**} />
<DropdownControl
staticProp={{ labelTitle: 'Is this related to an OCIO Project?', required: true }}
dynamicProp={{ options: signatureRequiredLegal }} />
</div>
</fieldset>
</div>
</form>
</div>
</div>
);
}
}
You forgot to wait for the fetch to end:
// Mark the function as async
public async componentWillMount(): Promise<void> {
this.eRequestService = new ERequestService(
this.props.context.pageContext.web.absoluteUrl,
this.props.context.spHttpClient
);
this.appSettings = new AppSettings();
this.serviceCalls = new NwhERequestFormRest(this.eRequestService, this.appSettings);
// Wait for the fetch ↓↓↓↓↓
this.includeDocReqSig = await this.serviceCalls._getFieldChoice("Signature_x0020_Required", true);
console.log(this.includeDocReqSig);
// return Promise.resolve(); ← This line is optional since the function is marked as async
}
But I suggest to use a state manager instead of a class attribute.
For example with Redux you will be able to detect a change in the choices of the dropdown. As it is now, changes can't be detected as the attribute is out of the React loop.
I'm trying to work with localstorage in angular 2. I'm using angular cli.
app.component.ts
export class AppComponent implements OnInit {
currentItem: string;
newTodo: string;
todos: any;
constructor(){
this.currentItem = (localStorage.getItem('currentItem')!==null) ? JSON.parse(localStorage.getItem('currentItem')) : [ ];
localStorage.setItem('currentItem', JSON.stringify(this.currentItem));
this.newTodo = '';
this.todos = [];
}
addTodo() {
this.todos.push({
newTodo: this.newTodo,
done: false
});
this.newTodo = '';
localStorage.setItem('currentItem', JSON.stringify(this.todos));
}
ngOnInit(): void {}
}
app.component.html
<div>
<form (submit)="addTodo()">
<label>Name:</label>
<input [(ngModel)]="newTodo" class="textfield" name="newTodo">
<button type="submit">Add Todo</button>
</form>
</div>
<ul class="heroes">
<li *ngFor="let todo of todos; let i=index ">
<input type="checkbox" class="checkbox" [(ngModel)]="todo.done" />
<span [ngClass]="{'checked': todo.done}">{{ todo.newTodo }}</span>
<span (click)="deleteTodo(i)" class="delete-icon">x</span>
</li>
</ul>
<div>
<button (click)="deleteSelectedTodos()">Delete Selected</button>
</div>
It's a simple ToDo list, but it doesn't persist the data when I reload page.
In chrome inspect > Application > Local Storage I see the data. when I reload page, the data still appears, but it doesn't appears on view and when I add a new todo item, the Local Storage delete old items and update with a new todo.
Does anyone know how to fix it?
use your code like this
constructor(){
this.currentItem = (localStorage.getItem('currentItem')!==null) ? JSON.parse(localStorage.getItem('currentItem')) : [ ];
this.todos = this.currentItem;
}
addTodo() {
let local_items = localStorage.getItem('currentItem')
local_items.push({
newTodo: this.newTodo,
done: false
});
localStorage.setItem('currentItem', JSON.stringify(local_items));
this.newTodo = '';
}
Reason:
at the time of adding you set array in localStorage which has only latest object not old objects.
on refreshing page you are not assigning localStorage objects to todo variable
I modified a little the code provided for Pardeep Jain, and woked!
export class AppComponent implements OnInit {
currentItem: string;
newTodo: string;
todos: any;
constructor(){
this.currentItem = (localStorage.getItem('currentItem')!==null) ? JSON.parse(localStorage.getItem('currentItem')) : [ ];
this.todos = this.currentItem;
}
addTodo() {
this.todos.push({
newTodo: this.newTodo,
done: false
});
this.newTodo = '';
localStorage.setItem('currentItem', JSON.stringify(this.todos));
}
ngOnInit(): void {}
}