Using 2 Pages to filter a table in angular - html

I'm quite new to angular and wanted to know how to make it so i can have 1 page that you put the info you want to filter in the table and when you press "search" it will lead you to the second page where you see the table after its filtered.
i my question is odd but i really couldn't find any answer how to do this online.
I cant share code as its confidential to my work.
Something that looks like this site : https://maerskcontainersales.com/
I have tried using mock data but still couldn't put my head into the right thing to do.

There can be multiple ways how you can achieve this.
Using Provider
Suppose you have two pages and , serach-page is where you will enter your filters and result-page is where the table renders.
In search-page, you will create inputs( ex: textbox, dropdown etc ) and have ngModels for all of them, or you can use Angular reactive forms i.e FormGroup and FormControls. Users will select their input and click on search button, which will read values from models or controls and store them in the provider.
search-page.component.html
<form [formGroup]="searchForm" (submit)="search()">
<input formControlName="country" />
<input formControlName="city" />
...
<input type="submit">
</form>
search-page.component.ts
export class SearchPage {
...
search() {
const country = this.searchForm.get('country').value
...
// get rest of the values
...
this.searchService.setData({ country, city });
this.router.navigate(['/result']); // '/result' is path on the result-page
}
...
}
search.service.ts
#Injectable()
export class SearchService {
_data : any;
set data(val) {
this._data = val;
}
get data() {
return this._data;
}
}
result-page.component.ts
export class ResultPage {
...
ngOnInit() {
const filters = this.searchService.getData();
// filters will be your data from previous page
}
...
}
Using RouterParams
search-page.component.html
// same as before
search-page.component.ts
export class SearchPage {
...
search() {
const country = this.searchForm.get('country').value
...
// get rest of the values
...
this.router.navigate(['/result', { country, city }]); // '/result' is path on the result-page
}
...
}
result-page.component.ts
export class ResultPage {
...
constructor(route:ActivatedRoute) {
this.country = route.snapshot.paramMap.get("country")
// alternatively you can also do below
route.paramMap.subscribe(filters => {
// you will have your filters here
});
}
...
}
And once you have values of filters in result-page, use them to get data or filter data if already fetched, then render the table accordingly.
Let me know if I wasn't clear.

The simple solution I would suggest you to use a filter component and a results component a third container component. This component will get the filter criteria as an input variable and will output the filter criteria (using an output variable) when you press the "filter" button.
The container app will look like this:
<filterComponent (onFilter)="changeFilter($event)" [data]="someDate" *ngIf="!filterCriteria"></filterComponent>
<resultsComponent [data]="someDate" [filterCriteria]="filterCriteria" *ngIf="!!filterCriteria"></resultsComponent>
The filterCriteria that is sent to the second tableComponent will come from the eventEmmiter of the first tableComponent. The filterCriteria variable will be initiate to null and this will allow you to switch from one table to the other.

Related

Conditionally make a page read-only using react

I want to create a React webpage that has both editable and read-only versions, the whole page not just a few elements on the page. A version is displayed to the user based on user id and other conditions. How do I do it?
The only straight forward way I know is to create 2 pages one editable and one read-only and based on the condition show the appropriate version (html page) to the user.
Is there a better and smarter way to do this? Like can I create just one page for both versions and toggle the mode based on the condition to the users?
Your question should have provided an example of some code you had tried but based on the description, very rough example below of one of many possible solutions.
Suppose EditView component is your page and you are able to pass a value for permission based on whatever credential you need to apply.
Then you have a component, ExampleField that takes the permission and displays either an input or static text. A collection of multiple of these fields is mapped from a theoretical array of data that you'll have to fetch from somewhere and the fields are returned by the main component.
const EditView = ({permission}) => {
const [editable, setEditable] = useState();
const [values, setValues] = useState([]);
useEffect(() => {
setEditable(permission);
}, [permission]);
useEffect(() => {
//maybe fetch your data from a back end or whatever and assign it to `values`
//on page load
}, [])
const ExampleField = ({permission, val, index}) => {
const handleChange = (e) => {
let vals = [...values];
vals[index] = val;
setValues(vals);
}
return(
<>
{permission
? <input name="example" type="text" defaultValue={val}
onChange={handleChange} />
: <span>{val}</span>}
</>
)
}
const fields = values.map((value, i) => {
return <ExampleField permission={permission} val={value} index={i}/>
})
return(
<>
{fields}
</>
)
}
Most likely, you'll want to break out various field components into their own file and, instead of using useState, you would probably want to explore useContext or useStore type functionality to lift up your state and do all the react things.
*Haven't tested or even compiled this code - for illustration purposes only.

how to integrate validity of nested form in the main form

I have a component A which looks like this
In summary, a user can create different sections/answers and can save them. A rectangular button is created for each saved answer. Internally, all this is saved in Forms and is validated. I am using ace-editor which already provides capability to use the editor as form control.
snippet from A.ts
createForm() {
this.codeEditorForm = this.fb.group({
answer: [null, [this.validateThereIsAtleastOneSavedAnswer(this.answers),this.validateThereIsNoUnsavedAnswer(this.answers)]],
});
}
snippet from A.html
<ace-editor id="editor" class="form-control" formControlName="answer" [ngClass]="validateField('answer')" [(text)]="text"></ace-editor>
I want to use this component as a form control in other components. For eg. I have another component B which also has a form
B.ts
this.bForm = this.fb.group({
field1: [null],
field2: [null],
field3: [null, Validators.required],
field4: [null],
field5: [null], //the value of A maps to this field of the form in B
field6: [null]
},);
}
B.html
<A #a [readonlyFormStatus]="readonlyFormStatus" (answerSectionsEmitter)="handleAEvent($event)" class="form-control" formControlName="field5" [ngClass]="validateField('field5')" ></A>
I want that when bform is submitted only when validation of both bForm and aForm have passed.
What would be the right way to do this following Angular design philosophy?
The correct way seems to be that A implements ControlValueAccessor interface.
export class A implements OnInit, AfterViewInit, ControlValueAccessor {
...
...
}
"There’s the DefaultValueAccessor that takes care of text inputs and textareas, the SelectControlValueAccessor that handles select inputs, or the CheckboxControlValueAccessor, which, surprise, deals with checkboxes, and many more. So for these UI elements, we don't need to create value accessors but for custom components, we need to create a custom accessor" - https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html
Explanation - I am asking formB to take value of A and map it to field5 of formB. But Angular doesn't know what is the value of A. For input fields, Angular already knows that the value of the text box is the value which gets mapped to a form control. But for custom components, we have to explicitly tell Angular what is the value the custom components generates which gets mapped to a form's field. This is done by implementing ControlValueAccess interface.
The interface has 3 important methods.
1) writeValue which is way to tell how the UI changes if the model changes. Say UI of the custom component was a slider with left-end meaning 0% and right-end meaning 100%. If the model changes to say a value say 10/100 then the UI needs to slide to 10%. Update this method to change the UI. In my case, I didn't need to do anything in it because the data input direction in my case is UI to model and not model to UI (my model doesn't create text which needs to be filled in the text region.
writeValue(value:any){
console.log('write value called with value ',value);
}
2) registerOnChange - this is reverse of writeValue. Whenever the UI changes, the model needs to be changed as well. In my case, whenever user writes in textbox then I want to update my model. "Angular provides you with a function and asks you to call it whenever there is a change in your component with the new value so that it can update the control." - https://netbasal.com/angular-custom-form-controls-made-easy-4f963341c8e2
In my case, I want to propogate changes then A's save button is clicked (onSaveAnswer is called then). I want to propogate value of all saved answers at this time
answers:Array<AnswerInformation>;
propagateChange = (_: any) => {};
registerOnChange(fn) {
console.log('registerOnchange called');
this.propagateChange = fn;
}
inSaveAnswer(){
...
this.propagateChange(this.answers);
}
The value that gets propogated gets mapped to the form field to which A is mapped to.
<A #a [readonlyFormStatus]="readonlyFormStatus" (answerSectionsEmitter)="handleAEvent($event)" class="form-control" formControlName="field5" [ngClass]="validateField('field5')" ></A>
field5 will contain the values proporated (this.answers). its structure will be Array<AnswerInformation>; i.e. field5:Array<AnswerInformation>;
I could put addition verification that field5 is not an empty array like so
field5: [null, this.validateField5IsProvided]
validateField5IsProvided(control:AbstractControl) {
const f5:Array<AnswerInformation> = control.value;
if(f5){
if(f5.length !== 0){
// console.log('answer field is valid');
return null;
} else {
return {
validateAnswerIsSaved: { // check the class ShowErrorsComponent to see how validatePassword is used.
valid: false,
message: 'The field can\'t be empty. Please make sure to save the field'
}
};
}
} else {
return {
validateAnswerIsSaved: {
valid: false,
message: 'The fieldcan\'t be empty. Please make sure to save the field'
}
};
}
}
There are couple of more functions that need to be implemented as well
registerOnTouched() {
console.log('registerOnTouched called');
}
setDisabledState(isDisabled: boolean): void {
console.log('set disabled called with value ',isDisabled);
this.editor.setReadOnly(isDisabled);
}

Angular 6 - How to stop infinite polling in subscribe()

So I want to show an icon based on whether or not the number of projects in my list is > 3. I am using this getProjects() function that I need to subscribe to in order to get the data. I am setting a boolean when I subscribe that checks the number of projects in the list, then in my HTML, I use a ngIf to show the icon based on the boolean. I am able to get it to show correctly, however, I think I am constantly polling in my subscribe, and setting this boolean over and over again because it is making my webpage run really slow.
I have already tried the take(1) method which doesnt seem to stop the subscription, as well as set it to a "this.variable" scope inside my component. I am currently using event emitters however that is not working either.
This is my code so far,
Function that I subscribe to (in a different component):
getProjects(): Observable<ProjectInterfaceWithId[]> {
const organizationId = localStorage.getItem('organizationId');
return this.firestoreService.collection('organizations').doc(organizationId)
.collection('projects').snapshotChanges()
.pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as ProjectInterface;
const id = a.payload.doc.id;
return {id, ...data} as ProjectInterfaceWithId;
})),
map(list => {
if (list.length !== 0) {
this.buildProjectLookup(list);
this.projects = list;
return list;
}
})
);
}
Function that i use to get the data and set the boolean:
#Input() toggle: boolean;
#Output() iconStatus = new EventEmitter();
displayIcon() {
this.projectService.getProjects()
.pipe(take(1))
.subscribe(
list => {
if(list.length >= 3){
this.toggle = true;
this.iconStatus.emit(this.toggle);
}
});
}
HTML:
<i *ngIf="displayIcon()" class="material-icons">list</i>
Is there any way for me to literally just check the list length once so I don't get caught in this subscription loop? Thank you in advance!
It looks like it could be happening due to the ngIf referring to the displayIcon() method.
Every time change detection runs within your component, this method will be called. If your component is using default change detection, this will be very often.
see https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/ for more
One way this could be fixed is by making the ngIf refer to a variable instead.
For example, you could set a projects$ observable using
this.projects$ = this.projectService.getProjects()
.pipe(
take(1),
tap(projects => this.iconStatus.emit(projects.length >= 3))
);
This observable should likely be instantiated in your ngOnInit() method.
Then in your template you can use
<i *ngIf="(projects$ | async)?.length >= 3" class="material-icons">list</i>

Keyup event fire multipletime

Currently, I am working on Angular 4 app. In my component Html, I have one textbox. Whenever user first type anything I want to make an API call to get some data.
The issue is if User type 'A' then it is working fine and calling API. But when user type "ABC" it is making API call 3 times. Instead of making API call for every letter, only one call should be made.
Please suggest any solution.
Component's HTML :
<input id="inputbox" (keyup)="keyUp($event)"/>
Component :
data: string[]
keyUp(event: any) {
this.loadDataApiCall();
}
loadDataApiCall() {
// calling api to load data.
//fill data into
}
Can I solve this issue with help of RXjs in angular 4
Observable.fromEvent(yourDomElement, 'keyup').auditTime(100).subscribe(()=>{
doSomething();
});
You should probably add a timeout to your call and clear it every time it is triggered so only the last call is called.
data: string[]
keyUp(event: any) {
window.clearTimeout(window.apiCallTimeout);
window.apiCallTimeout = window.setTimeout(this.loadDataApiCall, 100);
}
loadDataApiCall() {
// calling api to load data.
//fill data into
}
This means of course that the call will be done 100ms after the user stops typing. Also if he types "a" and after a while he types "bc", then two calls will be made. Of course you can increase the delay to meet your requirements.
If you only want one API call you can use the blur event, which is emitted when the control loses focus:
<input id="inputbox" (blur)="keyUp($event)"/>
Try this:
keyUp(event: any) {
this.loadDataApiCall();
event.stopImmediatePropagation();
}
the right way to implement this is by registering the event and calling the API after sometime while saving the latest value and checking that the last registered value matches the latest registered value
so in your keyup
keyUp(event: any) {
this.latestValue = event.target.value;
this.registerApiCall(event.target.value);
}
register func
registerApiCall(value){
setTimeout(this.loadDataApiCall.bind(this), 500, value)
}
api call
loadDataApiCall(value) {
if (this.latestValue == value ){
// calling api to load data.
//fill data into
}
}
see working example in this plnk
EDIT:
Observable.fromEvent(yourDomElement, 'keyup').auditTime(100).subscribe(()=>{
doSomething();
});
by 陈杨华 is the RxJs implementation that looks much better, and here is a working plnkr
If you're willing to change your form to Reactive Forms this would be extremely easy
this.form.get("input").valueChanges.debounceTime(1000).subscribe((value) => {});
Reactive Forms gives you access to observables of value changes and status changes. We're basically subscribing to that observable which emits the value any time it changes and we add a delay of one second so that if the user is still typing and changing the value then it will not execute the code in our subscribe.
#Component({
selector: 'my-app',
template: `
<div>
<input type="text" (keyup)='keyUp.next($event)'>
</div>
,
})
export class App {
name:string;
public keyUp = new Subject<string>();
constructor() {
const subscription = this.keyUp
.map(event => event.target.value)
.debounceTime(1000)
.distinctUntilChanged()
.flatMap(search => Observable.of(search).delay(500))
.subscribe(console.log);
}
}

Need to filter content on page by single array property on user corresponding button click in Angular 2

So, I am very confused here. I want to filter books that are already presented on the page by a value of a property from its array and that property is its specific Category(Philosophy, Classic, Poetry, etc...) when user click on the specific corresponding button on the panel.
Here is source-code: https://github.com/EgomortIncognitus/bookstore
This is my first Angular project, and I am quite a beginner in all of this, so I know that StackOverflow is not a "write code for me" service, but I genuinely want to understand this in depth as I do not have a single idea how to do this. Please, if you can provide me step-by-step examples I would be truly grateful to learn from you. Thank you in advance, big time.
You will do the filtering on your book-listing.component.ts:
You declare an array of categories which we instantiate with a first value 'All' so later we'll be able to reset the filter and to show all books:
categoryArray = ['All'];
Then, to be able to go back and forth with filtering we keep the books array only for display filtered books and we declare another booksDatasource array where we are going to keep the books list unmodified, as received from the service. This way, each time we filter on booksDatasource and add the filtered result to books for display.
booksDatasource: Array<any> = [];
After this, we modify the ngOnInit like this:
ngOnInit() {
this.booksService.getAllBooks()
.subscribe(
data => {
this.booksDatasource = data;
this.books = this.booksDatasource;
this.fillCategory(this.booksDatasource, this.categoryArray);
},
error => this.error = error.statusText
);
}
First, we fill the booksDatasource with all data, then we bind booksDatasource to books for the first time to show all books and we call the new added method fillCategory in which we iterate the datasource and extract all categories we have and add then to categoryArray:
fillCategory(data, categoryArray) {
for (const book of data) {
if (categoryArray.indexOf(book.category) === -1) {
categoryArray.push(book.category);
}
}
}
On book-listing.component.html, we add a new select that will display all the categories we have on categoryArray and we bind onChange event to trigger the categoryChanged() on component:
<div class="col-sm-4">
<label for="sort-field">Category</label>
<select (change)="categoryChanged($event.target.value)" name="cat-field" id="cat-field" class="form-control">
<option
*ngFor="let cat of categoryArray"
[value]="cat">
{{ cat }}
</option>
</select>
</div>
On the categoryChanged(cat) we pass the selected category from the select and we filter the booksDatasource for books having the passed category and we add them to books array for display:
categoryChanged(cat) {
if (cat === 'All') {
this.books = this.booksDatasource;
} else {
this.books = this.booksDatasource.filter(item => {
if (item.category === cat) {
return true;
}
return false;
}
);
}
}
I forked your repository, added the working code and made a pull request with the changes so you'll have all this working on your repo.