I am using ace-editor in my angular app as a JSON editor, ace editor has a feature to detect any missing symbol ([], {}, "", , etc....) as per https://ace.c9.io/build/kitchen-sink.html
Result sceenshot
I found a post where it is suggesting to use webpack but I couldn't achieve the same as shown in screenshot.
Added following lines of code
import "ace-builds/webpack-resolver";
ace.config.setModuleUrl('ace/mode/json_worker', require('file-loader!ace-builds/src-noconflict/worker-json'))
ace.config.setModuleUrl('ace/mode/html', require('file-loader!ace-builds/src-noconflict/mode-html.js'))
Can any help me to identify the issue, Am i missing any import or dependency?
https://github.com/ajaxorg/ace-builds/issues/129
https://github.com/ajaxorg/ace-builds/blob/7489e42c81725cd58d969478ddf9b2e8fd6e8aef/webpack-resolver.js#L234
editor.ts
import {
Component, ViewChild, ElementRef, Input, Output, EventEmitter,
OnChanges, SimpleChanges
} from '#angular/core';
import * as ace from 'ace-builds';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-github';
import "ace-builds/webpack-resolver";
ace.config.setModuleUrl('ace/mode/json_worker', require('file-loader!ace-builds/src-noconflict/worker-json'))
ace.config.setModuleUrl('ace/mode/html', require('file-loader!ace-builds/src-noconflict/mode-html.js'))
const THEME = 'ace/theme/github';
const LANG = 'ace/mode/json';
export interface EditorChangeEventArgs {
newValue: any;
}
#Component({
selector: 'app-editor',
templateUrl: './editor.component.html',
styleUrls: ['./editor.component.css']
})
export class EditorComponent implements OnChanges {
#ViewChild('codeEditor') codeEditorElmRef: ElementRef;
private codeEditor: ace.Ace.Editor;
#Input() jsonObject;
#Input() readMode;
#Output() change = new EventEmitter();
data: any;
mode: any;
constructor() { }
ngOnChanges(changes: SimpleChanges) {
for (const properties of Object.keys(changes)) {
if (properties == 'jsonObject') {
const currentJSONObject = changes[properties];
if (currentJSONObject.currentValue && currentJSONObject.firstChange == false)
this.codeEditor.setValue(JSON.stringify(currentJSONObject.currentValue, null, '\t'), -1);
else
this.data = currentJSONObject.currentValue
}
if (properties == 'readMode') {
const currentReadMode = changes[properties];
if (currentReadMode.firstChange == false)
this.codeEditor.setReadOnly(currentReadMode.currentValue);
else
this.mode = currentReadMode.currentValue
}
}
}
ngAfterViewInit() {
const element = this.codeEditorElmRef.nativeElement;
const editorOptions: Partial<ace.Ace.EditorOptions> = {
highlightActiveLine: true,
displayIndentGuides: true,
highlightSelectedWord: true,
};
this.codeEditor = ace.edit(element, editorOptions);
this.codeEditor.setTheme(THEME);
this.codeEditor.getSession().setMode(LANG);
this.codeEditor.setShowFoldWidgets(true);
this.codeEditor.setHighlightActiveLine(true);
this.codeEditor.setShowPrintMargin(false);
if (this.data)
this.codeEditor.setValue(JSON.stringify(this.data, null, '\t'), -1);
this.codeEditor.setReadOnly(this.readMode);
if (this.mode)
this.codeEditor.setReadOnly(this.mode);
}
ngAfterViewChecked() {
this.codeEditor.setOptions({
maxLines: this.codeEditor.getSession().getScreenLength(),
autoScrollEditorIntoView: true
});
this.codeEditor.resize();
}
onChange(updatedJSON) {
this.change.emit({ newValue: updatedJSON });
}
}
HTML
<div ace-editor #codeEditor [autoUpdateContent]="true" [durationBeforeCallback]="1000" (textChanged)="onChange($event)"
(change)="onChange(codeEditor.value)" class="editor">
</div>
I solved it by enabling the syntax checker:
private getEditorOptions(): Partial<ace.Ace.EditorOptions> & { enableBasicAutocompletion?: boolean; } {
const basicEditorOptions: Partial<ace.Ace.EditorOptions> = {
useWorker:true,
highlightSelectedWord: true,
minLines: 20,
maxLines: 35,
};
const margedOptions = Object.assign(basicEditorOptions);
return margedOptions;
}
In your editor options, you need to enable the syntax checker
to enable that you need to use
useWorker:true
Related
I have a SPA that ultimately lists out a lot of data, but in batches.
I created a component at the bottom of the list, with a 'Visibility' directive so that when it is visible we make a new request to the dataset in a SQL server to get the next batch.
html-tag-for-component
<app-infinity-scroll
[(pageNumber)]="page"
[displayPage]="displayPage"
[authd]="authd"
[done]="done"
[numResults]="displayPage == 'tiles-hub' ? hubs.length : wallets.length"
class="{{scrollVisible ? '' : 'hiddenDisplay'}}"
trackVisibility
></app-infinity-scroll>
component-to-trigger-data-call
import { outputAst } from '#angular/compiler';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { DbSqlService } from 'services/db-sql.service';
import { TokenAuthService } from 'services/token-auth.service';
import { TrackVisibilityDirective } from 'src/app/directives/track-visibility.directive';
import { SortStyle } from 'src/app/interfaces/mvlot';
import { MatProgressBar } from '#angular/material/progress-bar';
#Component({
selector: 'app-infinity-scroll',
templateUrl: './infinity-scroll.component.html',
styleUrls: ['./infinity-scroll.component.scss']
})
export class InfinityScrollComponent implements OnInit {
#Input() pageNumber: number;
#Input() displayPage: string;
#Input() authd: boolean;
#Input() done: boolean;
#Input() numResults: number;
#Output() pageNumberChange = new EventEmitter<number>();
lastDisplay = '';
loading: boolean = true;
constructor(
private visTrack: TrackVisibilityDirective
, private cdr: ChangeDetectorRef
, private dbApi: DbSqlService
, private authService: TokenAuthService
) {
}
ngOnInit(): void {
this.authService.UserAuthd.subscribe((res) => {
// if (res) {
this.dbApi.initGetWalletsHandler(0, 50, SortStyle.scoreDesc);
this.pageNumber = 1;
// }
})
this.visTrack.visibile.subscribe((val) => {
if (!this.done) {
this.loading = true;
if (val) {
if (this.displayPage == 'tiles') {
this.dbApi.initGetWalletsHandler((this.pageNumber) * 50, 50, SortStyle.default);
this.pageNumber += 1;
}
if (this.displayPage == 'tiles-hub') {
this.dbApi.initGetHubsHandler((this.pageNumber) * 50, 50);
this.pageNumber += 1;
}
}
}
})
}
}
Some functions run, call out to a back-end, respond with data, where a listener is waiting.
this.dbApi.resultObs.subscribe(val => {
if (val.append != true) {
this.results = [];
}
if (val.reset) {
this.page = 1;
}
val.data.data.forEach((b: any) => {
var result: Collection;
var existingResults = this.results.filter(w => w.ownerId == b.ownerId);
if (existingResults.length == 0) {
result = {
ownerId: b.ownerId
, totalScore: b.modifiedLandScore
, filteredCount: b.filteredCount
, totalLots: b.totalLots
, totalPrice: b.totalPrice
, name: ''
, lands: []
, type: CollectionType.b
}
result.bs.push(b);
this.results.push(result);
} else {
result = existingResults[0];
result.bs.push(b);
}
});
this.resultDataSource = new MatTableDataSource(this.results);
this.collectionType = CollectionType.b;
this.uiService.loadingBar(false);
this.done = val.data.data.length == 0;
this.cdr.detectChanges();
})
And, finally this is laid out for the user:
<tr *ngFor="let result of results">
<td>
<display-block
[collection]="b"
[displayVertical]="displayVertical"
[displayCaseCount]="displayCaseCount"
[gridClassName]="gridClassName"
[authd]="authd"
[type]="result.type"
[expanded]="results.length == 1"
[isPhonePortrait]="isPhonePortrait"
></display-block>
</td>
</tr>
Everything works fine on the first grab of data.
And everything appears to work fine on the second pull, but for any of the items appended to the view with the second pull, ChangeDetector just seems to give up. I'll trigger an action, that should modify the view, but nothing happens, unless I manully put in cdr, or I flip to a new window, or something, then they respond.
I'm going to continue trying to find a root cause, but at the moment, I'm out of ideas. There's no prominent error message that would imply something broke. The items fromt the first batch still work. But the ones from the second will appear to lock up. until CDR is forced by an outside event.
I wanted to check here to see if anyone had any ideas on what may be causing this.
Also, here's the declaration code for 'trackVisibility'
import {
Directive,
ElementRef,
EventEmitter,
NgZone,
OnDestroy,
OnInit,
Output,
} from '#angular/core';
#Directive({
selector: '[trackVisibility]',
})
export class TrackVisibilityDirective implements OnInit, OnDestroy {
observer!: IntersectionObserver;
#Output()
visibile = new EventEmitter<boolean>();
constructor(private el: ElementRef<HTMLElement>, private ngZone: NgZone) {}
ngOnInit(): void {
this.ngZone.runOutsideAngular(() => {
this.observer = new IntersectionObserver((entries) => {
entries.forEach((e) => {
this.visibile.emit(e.isIntersecting);
});
});
this.observer.observe(this.el.nativeElement);
});
}
ngOnDestroy(): void {
this.observer.disconnect();
}
}
here is the solution
You used runOutsideAngular function in your Directive.
"Running functions via runOutsideAngular allows you to escape Angular's zone and do work that doesn't trigger Angular change-detection or is subject to Angular's error handling. Any future tasks or microtasks scheduled from within this function will continue executing from outside of the Angular zone."
I also changed some parts of the code for more readability.
I have created a global snackBarService in my angular application. I want to customise the panelClass based on the type of message (error, success, warning etc.). The approach I took is to have a global config in the constructor, which helps to define global styles/configurations for the snack bar and will add custom classes to change the background colours based on the message type.
SnackBarService.ts
import { Injectable, NgZone } from "#angular/core";
import { MatSnackBar, MatSnackBarConfig } from "#angular/material";
#Injectable({
providedIn: "root",
})
export class SnackbarService {
private config: MatSnackBarConfig;
constructor(private snackbar: MatSnackBar, private zone: NgZone) {
this.config = new MatSnackBarConfig();
this.config.panelClass = ["snackbar-container"];
this.config.verticalPosition = "top";
this.config.horizontalPosition = "right";
this.config.duration = 4000;
}
error(message: string) {
this.config.panelClass = ["snackbar-container", "error"];
this.show(message);
}
success(message: string) {
this.config.panelClass = ["snackbar-container", "success"];
this.show(message);
}
warning(message: string) {
this.config.panelClass = ["snackbar-container", "warning"];
this.show(message);
}
private show(message: string, config?: MatSnackBarConfig) {
config = config || this.config;
this.zone.run(() => {
this.snackbar.open(message, "x", config);
});
}
}
app.scss
.snackbar-container {
margin-top: 70px !important;
color: beige;
&.error {
background-color: #c62828 !important;
}
&.success {
background-color: #2e7d32 !important;
}
&.warning {
background-color: #ff8f00 !important;
}
}
And from the component I will be using the service like this
this.snackbarService.success("This message is from snackbar!!!");
The above code works perfectly.
But,
Since the panelClass does not have a .push method, I can't add dynamic classes, and because of this, I need to duplicate the global class every time like this this.config.panelClass = ["snackbar-container", "error"];
error(message: string) {
this.config.panelClass.push("error"); // this throws error in typescript
this.show(message);
}
Is there any better way to solve this problem?
Angular Material actually provides you a native way for setting a default config, so you don't need to instantiate the MatSnackBarConfig and then set its values. In the module you import the MatSnackBarModule (App/Shared/Material module), add the following:
import { MatSnackBarModule, MatSnackBarConfig, MAT_SNACK_BAR_DEFAULT_OPTIONS } from '#angular/material/snack-bar';
const matSnackbarDefaultConfig: MatSnackBarConfig = {
verticalPosition: 'top',
horizontalPosition: 'right',
duration: 4000,
};
#NgModule({
// ...
providers: [
{
provide: MAT_SNACK_BAR_DEFAULT_OPTIONS,
useValue: matSnackbarDefaultConfig,
},
],
})
export class MaterialModule { }
Then, you can use your service like this (I added a bit more of typing to it, feel free to remove them if you dont' like it and don't use strictNullChecks):
import { Injectable, NgZone } from '#angular/core';
import { MatSnackBar, MatSnackBarConfig } from '#angular/material/snack-bar';
// I actually recommend that you put this in a utils/helpers folder so you can use reuse it whenever needed
export const coerceToArray = <T>(value: T | T[]): T[] => (
Array.isArray(value)
? value
: [value]
);
#Injectable({
providedIn: 'root',
})
export class SnackbarService {
constructor(private snackbar: MatSnackBar, private zone: NgZone) { }
error(message: string): void {
this.show(message, { panelClass: ['snackbar-container', 'error'] });
}
success(message: string): void {
this.show(message, { panelClass: ['snackbar-container', 'success'] });
}
warning(message: string): void {
this.show(message, { panelClass: ['snackbar-container', 'warning'] });
}
private show(message: string, customConfig: MatSnackBarConfig = {}): void {
const customClasses = coerceToArray(customConfig.panelClass)
.filter((v) => typeof v === 'string') as string[];
this.zone.run(() => {
this.snackbar.open(
message,
'x',
{ ...customConfig, panelClass: ['snackbar-container', ...customClasses] },
);
});
}
}
Also, since your public methods don't accept other configs to passes (eg. duration), you can reduce your service to this:
import { Injectable, NgZone } from '#angular/core';
import { MatSnackBar } from '#angular/material/snack-bar';
// Just add the new required types here and TypeScript will require the public consumer to pass a valid type
export type SnackBarType = 'error' | 'success' | 'warning';
#Injectable({
providedIn: 'root',
})
export class SnackbarService {
constructor(private snackbar: MatSnackBar, private zone: NgZone) { }
show(message: string, type: SnackBarType): void {
this.zone.run(() => {
this.snackbar.open(
message,
'x',
{ panelClass: ['snackbar-container', type] },
);
});
}
}
You could do this:
(this.config.panelClass as string[]).push('error');
but you will be adding classes without removing the ones that are there already. You would still need to reset the array with the initial class every time:
this.config.panelClass = ['snackbar-container']
I have created a mat-table to display list of Jobs.
Now I want to add a mat-filter to search a job using date or JobId.
However the code that I have written doesn't seem to work.
It does not throw any errors and it doesn't filter data.
HTML Code:
<mat-form-field>
<input
matInput
(keyup)="applyFilter($event.target.value)"
placeholder="Search"
/>
</mat-form-field>
<mat-table [dataSource]="jobExecutionList">
...
Typescript Code:
jobExecutionList: any = [];
applyFilter(filterValue: string) {
this.jobExecutionList.filter = filterValue.trim().toLowerCase();
}
Whole Typescript file :
import { Component, OnInit } from "#angular/core";
import { MatTableDataSource } from "#angular/material";
import { GlobalAppSateService } from "../../services/globalAppSate.service";
import { DataService } from "../../services/data.service";
import { SnakBarComponent } from "../custom-components/snak-bar/snak-
bar.component";
import { DataSource } from "#angular/cdk/collections";
import { Observable, of } from "rxjs";
import {
animate,
state,
style,
transition,
trigger
} from "#angular/animations";
import { RecommendationService } from "../recommendation-service.service";
import { MessageService } from '../../services/message.service';
#Component({
selector: "app-job-execution-screen",
templateUrl: "./job-execution-screen.component.html",
styleUrls: ["./job-execution-screen.component.scss"],
animations: [
trigger("detailExpand", [
state(
"collapsed",
style({ height: "0px", minHeight: "0", visibility: "hidden" })
),
state("expanded", style({ height: "*", visibility: "visible" })),
transition(
"expanded <=> collapsed",
animate("225ms cubic-bezier(0.4, 0.0, 0.2, 1)")
)
])
]
})
export class JobExecutionScreenComponent implements OnInit {
displaySpinner: boolean = false;
jobId: string;
jobExecutionList: any = [];
jobExecStatDisplayedColumns = [
"jobId",
"executionDate",
"previousTimePeriod",
"afterTimePeriod",
"status",
"actions",
"spinner"
];
public selectedElem: any;
projectjobId: any = 1;
jobExecutionStat: any;
executionDate: string = new Date().toISOString().slice(0, 10);
executeJobStop: any;
changeStatus: any;
newStatus: any;
isExpansionDetailRow = (i: number, row: Object) =>
row.hasOwnProperty("detailRow");
expandedElement: any;
constructor(
private dataService: DataService,
public globalAppSateService: GlobalAppSateService,
private snakbar: SnakBarComponent,
private recommendationService: RecommendationService,
private messageService: MessageService
) {}
ngOnInit() {
const project = JSON.parse(this.dataService.getObject("project"));
if (project != null) {
this.globalAppSateService.onMessage(project);
}
// API to get list of Running Jobs
this.recommendationService
.getJobExecutionStatList(this.projectjobId)
.subscribe(data => {
this.jobExecutionList = data;
console.log(this.jobExecutionList);
// this.jobExecutionStat = new ExampleDataSource();
});
}
applyFilter(filterValue: string) {
this.jobExecutionList.filter = filterValue.trim().toLowerCase();
}
stop_exec_job(element) {
if (element.status == "Running" || element.status == "Pending") {
//Api to stop Job Execution
this.recommendationService
.stopJobExecution(element.jobId, "Cancelled")
.subscribe(data => {
this.executeJobStop = data;
//this.changeStatus.push(this.executeJobStop);
// this.newStatus = new ExampleDataSource();
});
this.displaySpinner = false;
element.status = "Cancelled";
this.snakbar.statusBar("Job Execution Stopped", "Sucess");
} else {
this.snakbar.statusBar("Job Failed to start", "Failure");
}
}
// Will need it for mat-progress bar
// stop_exec_job2() {
// this.stop_exec_job(this.selectedElem);
// this.displaySpinner = false;
// }
re_run_job(element) {
if (
element.status == "Cancelled" ||
element.status == "Completed" ||
element.status == "Executed" ||
element.status == "FINISHED"
) {
//Api to Re-Run Job Execution
this.recommendationService
.stopJobExecution(element.jobId, "Running")
.subscribe(data => {
this.executeJobStop = data;
//this.changeStatus.push(this.executeJobStop);
// this.newStatus = new ExampleDataSource();
});
this.displaySpinner = true;
element.status = "Running";
this.snakbar.statusBar("Job Execution Started", "Sucess");
this.messageService.messageReceived$.subscribe(data => {
this.snakbar.statusBar(
'Platform job status - ' + data,
'Info'
);
//this.isLoadingResults = false;
});
} else {
this.snakbar.statusBar("Job Failed to start", "Failure");
}
}
}
export interface Element {
jobId: number;
executionDate: string;
previousTimePeriod: string;
afterTimePeriod: string;
status: string;
}
This is the Whole typescript file.
Based on different comment, you need to do:
dataSource: MatTableDataSource<any>;
And then when you get the data:
this.dataSource = new MatTableDataSource(/** YOUR DATA **/);
In your example:
import { Component, OnInit } from "#angular/core";
import { MatTableDataSource } from "#angular/material";
import { GlobalAppSateService } from "../../services/globalAppSate.service";
import { DataService } from "../../services/data.service";
import { SnakBarComponent } from "../custom-components/snak-bar/snak-
bar.component";
import { DataSource } from "#angular/cdk/collections";
import { Observable, of } from "rxjs";
import {
animate,
state,
style,
transition,
trigger
} from "#angular/animations";
import { RecommendationService } from "../recommendation-service.service";
import { MessageService } from '../../services/message.service';
#Component({
selector: "app-job-execution-screen",
templateUrl: "./job-execution-screen.component.html",
styleUrls: ["./job-execution-screen.component.scss"],
animations: [
trigger("detailExpand", [
state(
"collapsed",
style({ height: "0px", minHeight: "0", visibility: "hidden" })
),
state("expanded", style({ height: "*", visibility: "visible" })),
transition(
"expanded <=> collapsed",
animate("225ms cubic-bezier(0.4, 0.0, 0.2, 1)")
)
])
]
})
export class JobExecutionScreenComponent implements OnInit {
displaySpinner: boolean = false;
jobId: string;
jobExecutionList: MatTableDataSource<any>;
jobExecStatDisplayedColumns = [
"jobId",
"executionDate",
"previousTimePeriod",
"afterTimePeriod",
"status",
"actions",
"spinner"
];
public selectedElem: any;
projectjobId: any = 1;
jobExecutionStat: any;
executionDate: string = new Date().toISOString().slice(0, 10);
executeJobStop: any;
changeStatus: any;
newStatus: any;
isExpansionDetailRow = (i: number, row: Object) =>
row.hasOwnProperty("detailRow");
expandedElement: any;
constructor(
private dataService: DataService,
public globalAppSateService: GlobalAppSateService,
private snakbar: SnakBarComponent,
private recommendationService: RecommendationService,
private messageService: MessageService
) {}
ngOnInit() {
const project = JSON.parse(this.dataService.getObject("project"));
if (project != null) {
this.globalAppSateService.onMessage(project);
}
// API to get list of Running Jobs
this.recommendationService
.getJobExecutionStatList(this.projectjobId)
.subscribe(data => {
this.jobExecutionList = new MatTableDataSource(data);
console.log(this.jobExecutionList);
// this.jobExecutionStat = new ExampleDataSource();
});
}
applyFilter(filterValue: string) {
this.jobExecutionList.filter = filterValue.trim().toLowerCase();
}
I already have an example for this kind, you can look over this.
Mat-Table-stackblitz
I have a directive with the following code
import { Directive, Input, OnInit, ElementRef, SimpleChanges, OnChanges } from '#angular/core';
import tippy from 'tippy.js';
#Directive({
selector: '[tippy]'
})
export class TippyDirective implements OnInit, OnChanges {
#Input('tippyOptions') public tippyOptions: Object;
private el: any;
private tippy: any = null;
private popper: any = null;
constructor(el: ElementRef) {
this.el = el;
}
public ngOnInit() {
this.loadTippy();
}
public ngOnChanges(changes: SimpleChanges) {
if (changes.tippyOptions) {
this.tippyOptions = changes.tippyOptions.currentValue;
this.loadTippy();
}
}
public tippyClose() {
this.loadTippy();
}
private loadTippy() {
setTimeout(() => {
let el = this.el.nativeElement;
let tippyOptions = this.tippyOptions || {};
if (this.tippy) {
this.tippy.destroyAll(this.popper);
}
this.tippy = tippy(el, tippyOptions, true);
this.popper = this.tippy.getPopperElement(el);
});
}
}
And using the directive as follows
<input tippy [tippyOptions]="{
arrow: true,
createPopperInstanceOnInit: true
}" class="search-input" type="text"
(keyup)="searchInputKeyDown($event)">
How can I have the Tippy shown on mouseenter or focus as these are the default triggers, from the tippy instance I have in the directive, this is what I get when I put console.log(this.tippy) on line 44
{
destroyAll:ƒ destroyAll()
options:{placement: "top", livePlacement: true, trigger: "mouseenter focus", animation: "shift-away", html: false, …}
selector:input.search-input
tooltips:[]
}
As I am getting an error when I try to use
this.popper = this.tippy.getPopperElement(el);
ERROR TypeError: _this.tippy.getPopperElement is not a function
How can I get this directive to work as I took it from a repo in github
https://github.com/tdanielcox/ngx-tippy/blob/master/lib/tippy.directive.ts
What is it that I am missing here, any help is appreciated, thanks
I'm not sure what they were trying to accomplish in the linked repo you have included. To get tippy.js to work though, you should be able to change the directive to the following:
import { Directive, Input, OnInit, ElementRef } from '#angular/core';
import tippy from 'tippy.js';
#Directive({
/* tslint:disable-next-line */
selector: '[tippy]'
})
export class TippyDirective implements OnInit {
#Input('tippyOptions') public tippyOptions: Object;
constructor(private el: ElementRef) {
this.el = el;
}
public ngOnInit() {
tippy(this.el.nativeElement, this.tippyOptions || {}, true);
}
}
Working example repo
This works with tippy.js 6.x
#Directive({selector: '[tooltip],[tooltipOptions]'})
export class TooltipDirective implements OnDestroy, AfterViewInit, OnChanges {
constructor(private readonly el: ElementRef) {}
private instance: Instance<Props> = null;
#Input() tooltip: string;
#Input() tooltipOptions: Partial<Props>;
ngAfterViewInit() {
this.instance = tippy(this.el.nativeElement as Element, {});
this.updateProps({
...(this.tooltipOptions ?? {}),
content: this.tooltip,
});
}
ngOnDestroy() {
this.instance?.destroy();
this.instance = null;
}
ngOnChanges(changes: SimpleChanges) {
let props = {
...(this.tooltipOptions ?? {}),
content: this.tooltip,
};
if (changes.tooltipOptions) {
props = {...(changes.tooltipOptions.currentValue ?? {}), content: this.tooltip};
}
if (changes.tooltip) {
props.content = changes.tooltip.currentValue;
}
this.updateProps(props);
}
private updateProps(props: Partial<Props>) {
if (this.instance && !jsonEqual<any>(props, this.instance.props)) {
this.instance.setProps(this.normalizeOptions(props));
if (!props.content) {
this.instance.disable();
} else {
this.instance.enable();
}
}
}
private normalizeOptions = (props: Partial<Props>): Partial<Props> => ({
...(props || {}),
duration: props?.duration ?? [50, 50],
});
}
Using this looks like:
<button [tooltip]="'Hello!'">Hover here</button>
<button [tooltip]="'Hi!'" [tooltipOptions]="{placement: 'left'}">Hover here</button>
You can also use the lifecyle hook ngAfterViewInit then you don't need the setTimeout.
public ngAfterViewInit() {
this.loadTippy();
}
I'm trying to figure out how to switch a variable in a group of child components
I have this component for a editable form control which switches between view states
import {
Component,
Input,
ElementRef,
ViewChild,
Renderer,
forwardRef,
OnInit
} from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
const INLINE_EDIT_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InlineEditComponent),
multi: true
};
#Component({
selector: 'inline-edit',
templateUrl: 'inline-edit.html',
providers: [INLINE_EDIT_CONTROL_VALUE_ACCESSOR],
})
export class InlineEditComponent implements ControlValueAccessor, OnInit {
#ViewChild('inlineEditControl') inlineEditControl: ElementRef;
#Input() label: string = '';
#Input() type: string = 'text';
#Input() required: boolean = false;
#Input() disabled: boolean = false;
private _value: string = '';
private preValue: string = '';
public editing: boolean = false;
public onChange: any = Function.prototype;
public onTouched: any = Function.prototype;
get value(): any {
return this._value;
}
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
writeValue(value: any) {
this._value = value;
}
public registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
constructor(element: ElementRef, private _renderer: Renderer) {
}
ngOnInit() {
}
}
<div>
<div [hidden]="!editing">
<input #inlineEditControl [required]="required" [name]="value" [(ngModel)]="value" [type]="type" [placeholder]="label" />
</div>
<div [hidden]="editing">
<label class="block bold">{{label}}</label>
<div tabindex="0" class="inline-edit">{{value}} </div>
</div>
</div>
I'm trying to create a simple directive to consume these components and change the editing flag to true
export class EditForm {
//I want to do something like this:
public toggleEdit(fn: () => {}): void {
var editableFormControls = $('#selector: 'inline-edit');
editableFormControls.forEach(control => control.editing = true)
}
}
I want to grab all of the ediiitable form controls and set the editing flag in all of them to true, how can I do this?
You might need to implement a service that keeps the state and all child component subscribe to the state and parent push changes there.
import {Component, NgModule, VERSION, Input} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
export class EditableService {
subject = new BehaviorSubject(true);
getAsObservable() {
return this.subject.asObservable();
}
}
#Component({
selector:'editable',
template: '<div>i am editable {{ x | async}}</div>'
})
export class Editable {
constructor(private editableService: EditableService) {
this.x = editableService.getAsObservable();
}
}
#Component({
selector: 'my-app',
template: `
<editable></editable>
<editable></editable>
<hr/>
<button (click)="change()">change</button>
`,
providers: [EditableService]
})
export class App {
change() {
this.editableService.subject.next(false);
}
constructor(private editableService: EditableService) {
this.name = `Angular! v${VERSION.full}`;
}
}