I know how to fix my component using a different name for the output value of this component.
let me share my code
import {Component, Input, Output, EventEmitter} from '#angular/core';
import {TranslationPipe} from "../pipes/translation.pipe";
#Component({
selector: 'msisdn-confirm',
template: `
<div class="msisdn-confirm">
<div>
<input type="text" [(ngModel)]="m1">
</div>
<div>
<input type="text" [(ngModel)]="m2">
</div>
<p class="error">{{message}}</p>
</div>
`
})
export class MsisdnConfirm {
message:string;
#Output('mobile') emitter: EventEmitter<string> = new EventEmitter<string>();
#Input('mobile') set setMobileValue(value) {
this.msisdn_confirm = this.msisdn = value;
}
set m1(value) {
this.msisdn = value;
if (this.valid()) {
console.log('emit' + this.msisdn);
this.emitter.emit(this.msisdn);
}
}
set m2(value) {
this.msisdn_confirm = value;
if (this.valid()) {
console.log('emit' + this.msisdn);
this.emitter.emit(this.msisdn);
}
}
get m1():string {
return this.msisdn;
}
get m2():string {
return this.msisdn_confirm
}
msisdn: string;
msisdn_confirm: string;
constructor() {
}
private valid(): boolean {
if (!/06[0-9]{8}/.test(this.msisdn)) {
this.message = new TranslationPipe().transform("Het mobiele nummer is incorrect, (bijvoorbeeld: 0612345678)")
return false;
} else if (this.msisdn != this.msisdn_confirm) {
this.message = new TranslationPipe().transform("De mobiele nummers komen niet overeen")
return false;
}
this.message = null;
return true;
}
}
So this is a very basic component which validates two strings to be a "valid" dutch Mobile number, so a confirm box so to say. Now I can get my value in the parent component by doing something like
(mobile)="myParam = $event"
What I want is to use it like
[(mobile)]="myParam"
This only works for setting though, is this not supported on custom components?
For this compact syntax to work the input and output need to follow specific naming rules
[(mobile)]="myParam"
#Output('mobileChange') emitter: EventEmitter<string> = new EventEmitter<string>();
#Input('mobile') set setMobileValue(value) {
this.msisdn_confirm = this.msisdn = value;
}
Renaming inputs and outputs by passing a string parameter to the decorator is discourages. Rather use
#Output() mobileChange: EventEmitter<string> = new EventEmitter<string>();
#Input() set mobile(value) {
this.msisdn_confirm = this.msisdn = value;
}
Another example of Gunter's code above that may help:
export class TaskBook {
public taskBookID: number;
public title: String;
}
Inside component code:
....
<input type="text" pInputText [(ngModel)]="data!.title" (change)="onDataChange()" />
....
#Component({
selector: 'taskbook_edit',
templateUrl: './taskbook_edit.component.html'
})
export class TaskbookEditComponent {
#Input() data: TaskBook;
#Output() dataChange = new EventEmitter<TaskBook>();
constructor() { }
onDataChange() {
this.dataChange.emit(this.data);
}
}
Outside in calling component:
<taskbook_edit [(data)]="taskbookObj" ></taskbook_edit>
public taskbookObj: TaskBook;
Related
Working with complete table example (with sorting, filtering and pagination) that simulates a server call using a JSON constant.
I am trying to make it call a real API that returns JSON instead of a local constant.
I was successful in changing the code from the demo from using countries to suppliers. For example countries.ts is suppliers.ts, country.ts is supplier.ts, and country.service is supplier.service.
That works with no problems, but I want to remove the countries.ts (suppliers.ts in my case), export the JSON and replace it with a http.get call to a local API service.
Here's a sample of a working code from the API service that I am trying to call:
getSuppliers(): Observable<SupplierVM[]> {
return this.http.get<SupplierVM[]>(apiUrl+'supplier')
.pipe(
tap(heroes => console.log('fetched Suppliers')),
catchError(this.handleError('getSuppliers', []))
);
}
Here is a sample of a working call from within an Angular component:
allSuppliers:Observable<SupplierVM[]>;
this.allSuppliers=this.api.getSuppliers();
This is the method that does the work in the demo (the only difference is that I am using suppliers instead of countries)
private _search(): Observable<SearchResult> {
const {sortColumn, sortDirection, pageSize, page, searchTerm} = this._state;
//1. sort
let suppliers = sort(SUPPLIERS, sortColumn, sortDirection);
//2. filter
suppliers = suppliers.filter(country => matches(country, searchTerm/*, this.pipe*/));
const total = suppliers.length;
//3. paginate
suppliers = suppliers.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);
return of({suppliers, total});
}
This works when I call suppliers from the import statement, but I want to replace the suppliers from the sort method to something like this.allSuppliers (kind of like the sample method call above).
//1. sort
let suppliers = sort(this.allSuppliers, sortColumn, sortDirection);
Everything works when using local imported constant composed of JSON and should work just the same when calling actual service because the JSON response is the exact same.
I had a go at this and ended up implementing a switchable server-side/client-side sorting/pagination/filtering system (Angular 12).
Base Service: All services are derived from this, even ones without sorting/pagination/filtering.
import { HttpClient, HttpParams } from "#angular/common/http";
import { Observable } from "rxjs";
export abstract class BaseService<T> {
constructor(protected _http: HttpClient, protected actionUrl: string) {
}
getAll(params?: HttpParams): Observable<T[]> {
if (params)
return this._http.get(this.actionUrl, { params: params }) as Observable<T[]>;
return this._http.get(this.actionUrl) as Observable<T[]>;
}
getSingle(id: number, params?: HttpParams): Observable<T> {
if (params)
return this._http.get(`${this.actionUrl}/${id}`, { params: params }) as Observable<T>;
return this._http.get(`${this.actionUrl}/${id}`) as Observable<T>;
}
push(content: any, id: number) {
if (id === 0)
return this._http.post<any>(`${this.actionUrl}`, content);
return this._http.post<any>(`${this.actionUrl}/${id}`, content);
}
post(content: any) {
return this._http.post<any>(`${this.actionUrl}`, content);
}
}
Sortable Service: Services with a sortable/paginated/filtered result set are always derived from this base service. Derived services need to implement their own matching algorithm (_matches()).
import { HttpClient } from "#angular/common/http";
import { BehaviorSubject, Observable, Subject } from "rxjs";
import { BaseService } from "./base.service";
import { SearchResult, SearchState } from "#app/common/_models";
import { SortDirection } from '#app/common/_directives';
import { debounceTime, delay, switchMap, tap } from "rxjs/operators";
export abstract class SortableService<T> extends BaseService<T> {
private _loading$ = new BehaviorSubject<boolean>(true);
private _search$ = new Subject<void>();
private _items$ = new BehaviorSubject<T[]>([]);
private _total$ = new BehaviorSubject<number>(0);
protected _state: SearchState = {
page: 1,
pageSize: 10,
searchTerm: '',
sortColumn: '',
sortDirection: ''
};
constructor(protected _http: HttpClient, protected actionUrl:string) {
super(_http, actionUrl);
this._search$.pipe(
tap(() => this._loading$.next(true)),
debounceTime(200),
switchMap(() => this._search()),
tap(() => this._loading$.next(false))
).subscribe(response => {
this._items$.next(response.result);
this._total$.next(response.total);
});
this._search$.next();
}
get items$() { return this._items$.asObservable(); }
get total$() { return this._total$.asObservable(); }
get loading$() { return this._loading$.asObservable(); }
get page() { return this._state.page; }
get pageSize() { return this._state.pageSize; }
get searchTerm() { return this._state.searchTerm; }
set page(page: number) { this._set({ page }); }
set pageSize(pageSize: number) { this._set({ pageSize }); }
set searchTerm(searchTerm: string) { this._set({ searchTerm }); }
set sortColumn(sortColumn: string) { this._set({ sortColumn }); }
set sortDirection(sortDirection: SortDirection) { this._set({ sortDirection }); }
private _set(patch: Partial<SearchState>) {
Object.assign(this._state, patch);
this._search$.next();
}
protected _compare(v1: string | number | boolean | T[keyof T], v2: string | number | boolean | T[keyof T]) {
return v1 < v2 ? -1 : v1 > v2 ? 1 : 0;
}
private _getValue<T>(obj: T, column: string) {
var parts = column.split('.');
var result : any = obj;
for (let part of parts) {
for (let res in result) {
if (res === part)
result = result[res];
}
}
return result;
}
protected _sort(items: T[], column: string, direction: string): T[] {
if (direction === '' || column === '')
return items;
return [...items].sort((a, b) => {
var aa = this._getValue(a, column);
var bb = this._getValue(b, column);
const res = aa === undefined ? -1 : bb === undefined ? 1 : this._compare(aa, bb);
return direction === 'asc' ? res : -res;
});
}
protected abstract _matches(items: T, term: string) : boolean;
protected abstract _search(): Observable<SearchResult<T>>;
}
Client-Sortable Service: If the list manipulation happens in the client, derive the services from this one. I use a simple 1-minute cache to hold data.
import { HttpClient } from "#angular/common/http";
import { Observable, of } from "rxjs";
import { mergeMap } from "rxjs/operators";
import { SortableService } from "./sortable.service";
import { SearchResult } from "#app/common/_models";
export abstract class ClientSortableService<T> extends SortableService<T> {
cache?: T[];
cacheUpdatedIn?: Date;
constructor(protected _http: HttpClient, protected actionUrl: string) {
super(_http, actionUrl);
}
private getCache(): Observable<T[]> {
var expected = new Date();
if (this.cacheUpdatedIn !== undefined) {
expected = this.cacheUpdatedIn;
expected.setMinutes(expected.getMinutes() + 1);
}
//Search again.
if (this.cache == undefined || this.cache.length == 0 || expected == undefined || expected < new Date())
{
this.cacheUpdatedIn = new Date();
return this.getAll();
}
return of(this.cache || []);
}
protected _search(): Observable<SearchResult<T>> {
return this.getCache().pipe(mergeMap(items => {
this.cache = items;
//1: Sort.
let siteGroups = this._sort(items, this._state.sortColumn, this._state.sortDirection);
//2: Filter.
siteGroups = siteGroups.filter(group => this._matches(group, this._state.searchTerm));
const total = siteGroups.length;
//3: Paginate.
siteGroups = siteGroups.slice((this._state.page - 1) * this._state.pageSize, (this._state.page - 1) * this._state.pageSize + this._state.pageSize);
return of({ result: siteGroups, total: total });
}));
}
}
Server-Sortable Service: If the list manipulation happens in the server, derive the services from this one. The api need to handle the results before returning it.
import { HttpClient } from "#angular/common/http";
import { Observable, of } from "rxjs";
import { SortableService } from "./sortable.service";
import { SearchResult } from "#app/common/_models";
export abstract class ServerSortableService<T> extends SortableService<T> {
constructor(protected _http: HttpClient, protected actionUrl: string) {
super(_http, actionUrl);
}
protected _search(): Observable<SearchResult<T>> {
return super.post(this._state);
}
}
Custom Service: This custom service derives from the client-side base service. See that I need to declare a method that is used for the filtering, to select which fields to compare to.
#Injectable({ providedIn: 'root' })
export class ExampleService extends ClientSortableService<Example> {
constructor(protected _http: HttpClient) {
super(_http, `${environment.apiUrl}/sites`);
}
protected _matches(items: Example, term: string): boolean {
return items.displayName.toLowerCase().includes(term.toLowerCase());
}
}
So I beat my head against this for over a day.. I had it but I was doing something silly that broke pagination.
Anyway, what I did was all client-side since my data isn't going to be huge. In the constructor I wrapped the search.next() call in the subscribe of the API call. I also assigned the results to an array there. That way I had data when the search.next() was called. That array is what the actual function manipulates.
If you can follow my bad code here are the relevant snippets:
Declare a new variable to hold the results.
private myData: MyDataType[] = [];
In the constructor add your API call to get your data.
this.httpClient.get<MyDataType[]>('GetMyData').subscribe(data => {
this.myData = data;
this._search$.next();
});
This will load your data client-side to be used by the search function and trigger the initial call to said function.
And finally, in the search function, use your newly created and data-loaded variable for sorting and filtering.
//1. Sort
let theData = sort(this.myData, sortColumn, sortDirection);
I don't know what I was trying to do at first.. I went around the world and googled for several hours and found nothing that made it seem so simple. Just a few lines of code was all it took!
Sorry for my english.
Data binding doesn't work.
All data correctly serializing and displayed, but if i try to change some value - nothing happens.
Klik() method working correctly, conditions works correctly.
Please, help.
HTML code
<div id="app">
<div class="areaInfo " v-for="area in mainObjects" v-on:click="klik(area)">
<div class="trDiv areaData">
<div class="tdDiv" v-for="(prop, key) in area" v-if="key != 'ChildData'">
{{key}}
<template v-if="key.includes('Start') || key.includes('End') ">
{{ ConvertJsonDateString(prop) }}
</template>
<template v-else-if="!key.includes('Id')">
{{ prop }}
</template>
</div>
<div class="tdDiv" > {{area.childSeen}}</div>
</div>
</div>
</div>
Script:
var mainObjects = #(Html.Raw(result.Content));
for (var i = 0; i < mainObjects.length; i++) {
mainObjects[i].childSeen = false;
for (var j = 0; j < mainObjects[i].ChildData.length; j++) {
mainObjects[i].ChildData[j].childSeen = false;
}
}
console.log(mainObjects);
var app = new Vue({
el: "#app",
data: mainObjects,
methods: {
klik: function (region) {
console.log(region.childSeen)
if (region.childSeen == false) {
console.log('wasFalse');
return region.childSeen = true;
}
return region.childSeen = false;
}
},
});
Model example:
public class Test
{
public string FirstName {get;set;}
public string LastName {get;set;}
public List<Rebenok> ChildData {get;set;}
}
public class Rebenok
{
public string FirstName {get;set;}
public string LastName {get;set;}
public List<Diagnosis> Diagnoses {get;set;}
}
public class Diagnosis
{
public string Name {get;set;}
public string Description {get;set;}
}
mainObjects reference is not changed. You need to deep copy to make Vue reactive
<div id="app">
<div class="areaInfo " v-for="(area, index) in mainObjects" v-on:click="klik(index)">
<div class="trDiv areaData">
<div class="tdDiv" v-for="(prop, key) in area" v-if="key != 'ChildData'">
{{key}}
<template v-if="key.includes('Start') || key.includes('End') ">
{{ ConvertJsonDateString(prop) }}
</template>
<template v-else-if="!key.includes('Id')">
{{ prop }}
</template>
</div>
<div class="tdDiv" > {{area.childSeen}}</div>
</div>
</div>
</div>
var app = new Vue({
el: "#app",
data () {
return {
mainObjects
}
},
methods: {
klik: function (index) {
const mainObjects = JSON.parse(JSON.stringify(mainObjects)) // deep copy
const region = mainObjects[index]
console.log(region.childSeen)
if (region.childSeen == false) {
console.log('wasFalse');
region.childSeen = true;
}
region.childSeen = false;
this.mainObjects = mainObjects // assign again
}
},
});
I am facing some issue with canDeactivate() whenever it returns false it is changing the window history as when it became true and if i hit the back button. I am navigating to some other URL or out of the app itself.
Please help me
Here is the issue, but it still isn't fixed.
As a workaround, you can manually put the active url back to the history:
export class CanDeactivateGuard implements CanDeactivate<any> {
constructor(
private readonly location: Location,
private readonly router: Router
) {}
canDeactivate(component: any, currentRoute: ActivatedRouteSnapshot): boolean {
if (myCondition) {
const currentUrlTree = this.router.createUrlTree([], currentRoute);
const currentUrl = currentUrlTree.toString();
this.location.go(currentUrl);
return false;
} else {
return true;
}
}
}
The issue is still present with Angular routing and below is what worked for me.
Trick is to use this.router.navigate([currentUrl], { skipLocationChange: true });
Full code:
export class CanDeactivateGuard implements CanDeactivate<any> {
constructor(
private location: Location,
private router: Router
) { }
canDeactivate(component: any,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot): boolean {
if(canDeactivateCondition) {
return true;
} else {
const currentUrl = currentState.url;
if (this.location.isCurrentPathEqualTo(currentUrl)) {
// https://github.com/angular/angular/issues/13586
this.router.navigate([currentUrl], { skipLocationChange: true });
} else {
// A browser button has been clicked or location.back()/forward() invoked. Restore browser history
history.pushState(null, null, location.href);
}
return false;
}
}
}
This is a know bug in Angular: https://github.com/angular/angular/issues/13586
There is a lot a workaround proposed but all of them seem to break other parts.
Appears to be fixed now in Angular 12.1.2: https://github.com/angular/angular/issues/13586#issuecomment-881627456
I'm creating a custom component which is a list of Buttons.
When the user clicks on a button, i change its css class and then i would like to add it in a custom "selectedItems" property to retrieve it in my ViewModel.
When I do a push on the "selectedItems" array property, no event is raised and I don't get the information.
Also, I tried to re-set the entire array but not better.
I don't know how to achieve this.
Here is the code of my component :
import {WrapLayout} from "ui/layouts/wrap-layout";
import {EventData} from "data/observable";
import {ValueButton} from "./value-button";
import dependencyObservableModule = require("ui/core/dependency-observable");
export class ValuesSelector extends WrapLayout {
public static itemsProperty = new dependencyObservableModule.Property(
"items",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
[],
dependencyObservableModule.PropertyMetadataSettings.None,
function(data: dependencyObservableModule.PropertyChangeData) {
if (data.newValue) {
let instance = <ValuesSelector>data.object;
instance.items = data.newValue;
}
}));
public static deleteOnClickProperty = new dependencyObservableModule.Property(
"deleteOnClick",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
false,
dependencyObservableModule.PropertyMetadataSettings.None));
public static selectedItemsProperty = new dependencyObservableModule.Property(
"selectedItems",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
[],
dependencyObservableModule.PropertyMetadataSettings.None));
public static singleSelectionProperty = new dependencyObservableModule.Property(
"singleSelection",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
false,
dependencyObservableModule.PropertyMetadataSettings.None));
public get selectedItems() {
return this._getValue(ValuesSelector.selectedItemsProperty);
}
public set selectedItems(value: any[]) {
this._setValue(ValuesSelector.selectedItemsProperty, value);
}
public get deleteOnClick() {
return this._getValue(ValuesSelector.deleteOnClickProperty);
}
public set deleteOnClick(value: boolean) {
this._setValue(ValuesSelector.deleteOnClickProperty, value);
}
public get singleSelection() {
return this._getValue(ValuesSelector.singleSelectionProperty);
}
public set singleSelection(value: boolean) {
this._setValue(ValuesSelector.singleSelectionProperty, value);
}
public get items() {
return this._getValue(ValuesSelector.itemsProperty);
}
public set items(value: any) {
this._setValue(ValuesSelector.itemsProperty, value);
this.createUI();
}
private _buttons: ValueButton[];
constructor() {
super();
this.orientation = "horizontal";
this._buttons = [];
}
private createUI() {
this.removeChildren();
let itemsLength = this.items.length;
for (let i = 0; i < itemsLength; i++) {
let itemButton = new ValueButton();
itemButton.text = this.items[i].label;
itemButton.value = this.items[i];
itemButton.className = "values-selector-item";
if (this.deleteOnClick) {
itemButton.className = "values-selector-selected-item";
}
itemButton.on(ValueButton.tapEvent, (data: EventData) => {
let clickedButton = <ValueButton>data.object;
if (this.deleteOnClick) {
let itemIndex = this.items.indexOf(clickedButton.value);
if (itemIndex > -1) {
let newSelectedItems = this.items;
newSelectedItems.splice(itemIndex, 1);
this.items = newSelectedItems;
}
return;
}
let internalSelectedItems = this.selectedItems;
if (clickedButton.className === "values-selector-item") {
if (this.singleSelection && this.selectedItems.length > 0) {
internalSelectedItems = [];
for (let i = 0; i < this._buttons.length; i++) {
this._buttons[i].className = "values-selector-item";
}
}
internalSelectedItems.push(clickedButton.value);
clickedButton.className = "values-selector-selected-item";
} else {
let itemIndex = internalSelectedItems.indexOf(clickedButton.value);
if (itemIndex > -1) {
internalSelectedItems.splice(itemIndex, 1);
}
clickedButton.className = "values-selector-item";
}
this.selectedItems = internalSelectedItems;
}, this);
this._buttons.push(itemButton);
this.addChild(itemButton);
}
}
}
Can you help me ?
Thanks
Ok I made a mistake by databinding my property.
In fact, in the XML I use the component like this :
<vs:ValuesSelector items="{{ criterias }}" selectedItems="{{ myObject.selectedCriterias }}" />
But in the ViewModel, I never initialized the selectedCriterias property because I thought that the default value [] specified in the component would create it.
So in the ViewModel, here is the fix :
Before
this.myObject = {
id : 0
};
After
this.myObject = {
id : 0,
selectedCriterias: []
};
Can I determine the generic type T in the following scenario?
class MyClass {
constructor() {
}
GenericMethod<T>(): string {
return typeof(T); // <=== this is flagged by the compiler,
// and returns undefined at runtime
}
}
class MyClass2 {
}
alert(new MyClass().GenericMethod<MyClass2>());
Because the types are erased during compilation, they are not available when the code runs.
This means you have to make a small duplication...
class MyClass {
constructor() {
}
GenericMethod<T>(targetType: any): string {
return typeof(targetType);
}
}
class MyClass2 {
}
alert(new MyClass().GenericMethod<MyClass2>(MyClass2));
In this case, you end up with the answer function, but you probably wanted MyClass2.
I have written an example of how to get runtime type names in TypeScript, which looks like this:
class Describer {
static getName(inputClass) {
var funcNameRegex = /function (.{1,})\(/;
var results = (funcNameRegex).exec((<any> inputClass).constructor.toString());
return (results && results.length > 1) ? results[1] : "";
}
}
class Example {
}
class AnotherClass extends Example {
}
var x = new Example();
alert(Describer.getName(x)); // Example
var y = new AnotherClass();
alert(Describer.getName(y)); // AnotherClass