Unable to get nativeElement of ion-textarea in Ionic 4 to set height - angular-directive

I have a custom directive to adjust the ion-textarea height to autosize the height as text is entered rather than setting a fixed row height or having ugly scroll bars as the textarea fills up.
In Ionic-4 I am unable to get the nativeElement of the html textarea of the ion-textarea. Any help would be great
It's running on Angular 6 and Ionic 4 but when I try and get this.element.nativeElement.getElementsByTagName('textarea')[0] it is always undefined so I can't set the height programatically.
import { ElementRef, HostListener, Directive, OnInit } from '#angular/core';
#Directive({
selector: 'ion-textarea[autosize]'
})
export class AutosizeDirective implements OnInit {
#HostListener('input', ['$event.target'])
onInput(textArea:HTMLTextAreaElement):void {
this.adjust();
}
constructor(public element:ElementRef) {
}
ngOnInit():void {
setTimeout(() => this.adjust(), 0);
}
adjust():void {
const textArea = this.element.nativeElement.getElementsByTagName('textarea')[0];
textArea.style.overflow = 'hidden';
textArea.style.height = 'auto';
textArea.style.height = textArea.scrollHeight + 'px';
}
}
As the const textArea always comes back undefined I can't set the height to follow the scroll height to prevent the scroll bars.
Anyone been able to do this in Ionic-4? seen working examples in Ionic-3 as per the above code.
Thank you
Rowie

Below code would help your problem.
import { ElementRef, HostListener, Directive, AfterViewInit } from '#angular/core';
#Directive({
selector: 'ion-textarea[autosize]'
})
export class AutoSizeDirective implements AfterViewInit {
readonly defaultHeight = 64;
#HostListener('input', ['$event.target'])
onInput(textArea: HTMLTextAreaElement) {
this.adjust(textArea);
}
constructor(private element: ElementRef) {}
ngAfterViewInit() {
this.adjust();
}
adjust(textArea?: HTMLTextAreaElement) {
textArea = textArea || this.element.nativeElement.querySelector('textarea');
if (!textArea) {
return;
}
textArea.style.overflow = 'hidden';
textArea.style.height = 'auto';
textArea.style.height = (textArea.value ? textArea.scrollHeight : defaultHeight) + 'px';
}
}
Usage: <ion-textarea autosize></ion-textarea>
I have confirmed it on Ionic 4.0.2/Angular 7.2.6.
Regards.

this package does all the autosizing of my ion-textareas for me https://github.com/chrum/ngx-autosize
just follow the guide and get it working, if it doesn't work importing it into the app.module.ts then try importing it into the page's module, I personally needed that dunno if you will, but package is a life saver

Related

How to leave existing class attribute on image element - now it is being moved to a generated enclosing span

Background: Trying to use ckeditor5 as a replacement for my homegrown editor in a non-invasive way - meaning without changing my edited content or its class definitions. Would like to have WYSIWYG in the editor. Using django_ckeditor_5 as a base with my own ckeditor5 build that includes ckedito5-inspector and my extraPlugins and custom CSS. This works nicely.
Problem: When I load the following HTML into ClassicEditor (edited textarea.value):
<p>Text with inline image: <img class="someclass" src="/media/uploads/some.jpeg"></p>
in the editor view area, browser-inspection of the DOM shows:
...
<p>Text with an inline image:
<span class="image-inline ck-widget someclass ck-widget_with-resizer" contenteditable="false">
<img src="/media/uploads/some.jpeg">
<div class="ck ck-reset_all ck-widget__resizer ck-hidden">
<div ...></div></span></p>
...
Because the "someclass" class has been removed from and moved to the enclosing class attributes, my stylesheets are not able to size this image element as they would appear before editing.
If, within the ckeditor5 view, I edit the element using the browser inspector 'by hand' and add back class="someclass" to the image, ckeditor5 displays my page as I'd expect it with "someclass" and with the editing frame/tools also there. Switching to source-editing and back shows the class="someclass" on the and keeps it there after switching back to document editing mode.
(To get all this, I enabled the GeneralHtmlSupport plugin in the editor config with all allowed per instructions, and that seems to work fine.) I also added the following simple plugin:
export default class Extend extends Plugin {
static get pluginName() {
return 'Extend';
}
#updateSchema() {
const schema = this.editor.model.schema;
schema.extend('imageInline', {
allowAttributes: ['class']
});
}
init() {
const editor = this.editor;
this.#updateSchema();
}
}
to extend the imageInline model hoping that would make the Image plugin keep this class attribute.
This is the part where I need some direction on how to proceed - what should be added/modified in the Image Plugin or in my Extend plugin to keep the class attribute with the element while editing - basically to fulfill the WYSIWYG desire?
The following version does not rely on GeneralHtmlSupport but creates an imageClassAttribute model element and uses that to convert only the image class attribute and place it on the imageInline model view widget element.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
export default class Extend extends Plugin {
static get pluginName() {
return 'Extend';
}
#updateSchema() {
const schema = this.editor.model.schema;
schema.register( 'imageClassAttribute', {
isBlock: false,
isInline: false,
isObject: true,
isSelectable: false,
isContent: true,
allowWhere: 'imageInline',
});
schema.extend('imageInline', {
allowAttributes: ['imageClassAttribute' ]
});
}
init() {
const editor = this.editor;
this.#updateSchema();
this.#setupConversion();
}
#setupConversion() {
const editor = this.editor;
const t = editor.t;
const conversion = editor.conversion;
conversion.for( 'upcast' )
.attributeToAttribute({
view: 'class',
model: 'imageClassAttribute'
});
conversion.for( 'dataDowncast' )
.attributeToAttribute({
model: 'imageClassAttribute',
view: 'class'
});
conversion.for ( 'editingDowncast' ).add( // Custom conversion helper
dispatcher =>
dispatcher.on( 'attribute:imageClassAttribute:imageInline', (evt, data, { writer, consumable, mapper }) => {
if ( !consumable.consume(data.item, evt.name) ) {
return;
}
const imageContainer = mapper.toViewElement(data.item);
const imageElement = imageContainer.getChild(0);
if ( data.attributeNewValue !== null ) {
writer.setAttribute('class', data.attributeNewValue, imageElement);
} else {
writer.removeAttribute('class', imageElement);
}
})
);
}
}
Well, Mr. Nose Tothegrind found two solutions after digging through ckeditor5 code, here's the first one. This extension Plugin restores all image attributes that are collected by GeneralHtmlSupport. It can be imported and added to a custom ckeditor5 build app.js file by adding config.extraPlugins = [ Extend ]; before the editor.create(...) statement.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import GeneralHtmlSupport from '#ckeditor/ckeditor5-html-support/src/generalhtmlsupport';
export default class Extend extends Plugin {
static get pluginName() {
return 'Extend';
}
static get requires() {
return [ GeneralHtmlSupport ];
}
init() {
const editor = this.editor;
this.#setupConversion();
}
#setupConversion() {
const editor = this.editor;
const t = editor.t;
const conversion = editor.conversion;
conversion.for ( 'editingDowncast' ).add( // Custom conversion helper
dispatcher =>
dispatcher.on( 'attribute:htmlAttributes:imageInline', (evt, data, { writer, mapper }) => {
const imageContainer = mapper.toViewElement(data.item);
const imageElement = imageContainer.getChild(0);
if ( data.attributeNewValue !== null ) {
const newValue = data.attributeNewValue;
if ( newValue.classes ) {
writer.setAttribute('class', newValue.classes.join(' '), imageElement);
}
if ( newValue.attributes ) {
for (const name of Object.keys(newValue.attributes)) {
writer.setAttribute( name, newValue.attributes[name], imageElement);
}
}
} else {
writer.removeAttribute('class', imageElement);
}
})
);
}a
}

Vue Chart 3 - Doughnut Charts with Text in the Middle (Trouble registering a plugin)

EDIT: My codesandbox is working, but it's not perfect. In my actual project I've better utilized ts and am just making an if check for the charttype so the font doesn't paste on all the charts you have.
I still need to work on making the fontsize responsive and prettier, but I will try and update the codesandbox as much as I can!
If anyone would like to add to it please do! Also, post your link in the comments so anyone else having these issues can see them too!
I am attempting to make a doughnut chart that looks like this:
I've been trying to figure out how to get the code in this example using VueChartJS (VueChart3 is a TS safe rewrite of VueChartJS and works the same, just in composition API), but cannot figure out how to get the plugins to work properly.
vue-chartjs - Doughnut chart with text in the middle
Working Example from post
The example in the post above uses a textCenter() function and constantly access the context ctx variable. The first error I get however reads Property 'pluginService' does not exist on type 'typeof Chart' and also a Cannot read properties of undefined (reading 'register'). I think this has to do with the way VueChart3 and it's Chart.register(...registerables) line.
I've commented out all of the code that breaks the chart currently while I try and fix this issue.
I am really stumped on where to go with this now and could really use a bit guidance.
Cheers!
CodeSandbox Link
Chart2.vue
<template>
<div style="display: flex; justify-content: center; margin: 5rem">
<DoughnutChart :options="options" v-bind="doughnutChartProps" />
</div>
</template>
<script lang='ts'>
import { computed, ref, onMounted } from "vue";
import { DoughnutChart, useDoughnutChart } from "vue-chart-3";
import { Chart, ChartData, ChartOptions, registerables } from "chart.js";
Chart.register(...registerables, plugin);
var plugin = function (chart) {
var width = chart.chart.width;
var height = chart.chart.height;
var ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = 800;
var textX = Math.round((width - ctx.measureText(text).width) / 2);
var textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
};
export default {
name: "Home",
components: { DoughnutChart },
setup(props) {
const isResponsive = ref(true);
const getData = computed<ChartData<"doughnut">>(() => ({
labels: ["Success"],
datasets: [
{
data: [10, 90],
backgroundColor: ["#56cd92", "#f0f7ff"],
},
],
}));
onMounted(() => {
addPlugin({
id: "my-plugin",
beforeDraw: plugin,
});
// renderChart(chartdata, options);
// textCenter(1000);
});
const options = computed<ChartOptions<"doughnut">>(() => ({
plugins: {
legend: {
position: "bottom",
},
},
}));
const { doughnutChartProps } = useDoughnutChart({
options,
chartData: getData,
});
return {
isResponsive,
getData,
options,
doughnutChartProps,
};
},
};
</script>
This is because pluginService is V2 syntax, to register plugins globally in V3 you can do it the same way you did with the registerables like so:
Chart.register(plugin)
You can even do it in the same register call like so:
Chart.register(...registerables, plugin)
Edit:
Plugins also have to be objects so chart.js knows which hook to use as you did in the mounted so your plugin variable has to look like this (still V2 syntax, you will need to change this yourself) to work:
var plugin = {
id: 'idOfPlugin',
beforeDraw: function (chart) {
var width = chart.chart.width;
var height = chart.chart.height;
var ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = 800;
var textX = Math.round((width - ctx.measureText(text).width) / 2);
var textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
};
}

ion-slides methods not working in ionic 4

Unable to use methods provided by ion-slides in official documentation. checked other answers in here but all seems to confuse ionic 4 with ionic 3 and providing answers applicable in ionic 3.
I want to get active index of slide. online documentation is not complete about how to implement it.
Note: Use IonSlides and don't use ElementRef and nativeElement
Just follow the code below and it will work fine to get the active index from getActiveIndex()
import { IonSlides } from '#ionic/angular';
#ViewChild('slides', {static: true}) slides: IonSlides;
slideChanged(e: any) {
this.slides.getActiveIndex().then((index: number) => {
console.log(index);
});
}
In ionic 4, the return type of the getActiveIndex() method is Promise<number>, so the code you were using in ionic 3 will not work anymore. You could at a bare minimum switch it out for somehting like:
this.slider.getActiveIndex()
.then(activeIndex => {
console.log('active index = ', activeIndex );
if (activeIndex < this.slides.length) {
this.selectedSegment = this.slides[activeIndex ].id;
}
});
Or whatever you want to use it for. The official doc is actually pretty awesome on this: https://ionicframework.com/docs/api/slides
Build the slider in your html with a slides ID and a function which is emitted when the active slide has changed.
<ion-slides #slides (ionSlideDidChange)="getIndex()">
<ion-slide></ion-slide>
</ion-slides>
In the .ts file you import the slider ID with ViewChild and set the function to get the active index.
import { Component, OnInit, ViewChild } from '#angular/core';
import { Slides } from '#ionic/angular';
export class Page implements OnInit {
#ViewChild('slides') slides: Slides;
constructor() {}
ngOnInit() {
}
async getIndex() {
console.log(await this.slides.getActiveIndex());
}
}
I had the same issue, but I solved it with the following code:
My .ts file:
export class RegistroPage implements OnInit {
#ViewChild('registroWizard') registroWizard: IonSlides;
slideOpts: any;
constructor() {
this.slideOpts = {
effect: 'fade'
};
}
ngOnInit() {
this.registroWizard.lockSwipeToNext(true);
}
}
My HTML file:
<ion-slides #registroWizard pager="true" [options]="slideOpts">
<ion-slide>
<h1>Slide 1</h1>
<ion-button>Hola</ion-button>
</ion-slide>
<ion-slide>
<h1>Slide 2</h1>
<ion-button>Hola</ion-button>
</ion-slide>
<ion-slide>
<h1>Slide 3</h1>
<ion-button>Hola</ion-button>
</ion-slide>
</ion-slides>
I solved the problem like this:
page.ts:
import { IonSlides } from '#ionic/angular';
...
#ViewChild('slides') slides: IonSlides;
nextSlide() {
this.slides.slideNext();
}
page.html:
<ion-slides #slides pager="true" [options]="slideOpts">
<ion-slide>slide 1</ion-slide>
<ion-slide>slide 2</ion-slide>
</ion-slides>
<ion-button (click)="nextSlide()" class="register-buttons">go next</ion-button>
exact the same thing goes for the back action
static: true
#ViewChild('ionSlides', { static: true }) ionSlides: IonSlides;
u need declaration class to app.module.ts
#NgModule({
declarations: [MySliderComponent]
})
I used IonSlides as type but it didn't help.
For me, the above mentioned solutions didnt work (ionic v6.17.1). What worked was:
#ViewChild('slides', {static: true}) slides: ElementRef;
swipeRight() {
this.slides.nativeElement.slideNext();
All methods working this way. Altering ```{static: true} didn't throw any error
If you console.log after declaring slides as IonSlides type, it shows ElementRef type

ngx-chart error "TypeError: Object(...) is not a function"

I am trying to implements some statistics in my develepping platform and I try to use ngx-charts to display them. However I get an error and I can't figure out why.
I am using storedProcedures for MySQL statistics which I call from Java Restful Backend and return them in Angular 5 front-end. The returned table has the following two fields: Date and number of incidents per day. So the table returned by the backend has those two columns.
My code for the component rendering the chart is the following:
import {Component, OnInit} from '#angular/core';
import {StatisticsService} from '../../statistics.service';
class Data {
private _name: string;
private _value: number;
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
get value(): number {
return this._value;
}
set value(value: number) {
this._value = value;
}
}
#Component({
selector: 'app-daily-incidents-statistics',
templateUrl: './daily-incidents-statistics.component.html',
styleUrls: ['./daily-incidents-statistics.component.css']
})
export class DailyIncidentsStatisticsComponent implements OnInit {
view: any[] = [700, 400];
data: any[] = [];
// options
showXAxis = true;
showYAxis = true;
gradient = false;
showLegend = false;
showXAxisLabel = true;
xAxisLabel = 'Ημέρα';
showYAxisLabel = true;
yAxisLabel = 'Αρ. Περιστατικών';
constructor(private statisticsService: StatisticsService) {
// Object.assign(this, { single })
// Object.assign(this, { data } );
}
colorScheme = {
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
};
onSelect(event) {
console.log(event);
}
async ngOnInit() {
console.log('NG ON INIT EXECUTION');
await this.getIncidentsByDay();
}
getIncidentsByDay() {
this.statisticsService.getIncidentsByDay()
.subscribe(
(results) => {
let temp = new Data();
for (let i in results) {
console.log(results[i][0] + '>>=====>> ' + results[i][1]);
temp.name = results[i][0];
temp.value = results[i][1];
this.data.push(temp);
}
const test = this.data;
// for (let i = 0; i < this.data.length; i++) {
// console.log('wtf: ' + this.data[i][0] + '::::' + this.data[i][1]);
// }
// console.log(results);
// console.log(JSON.stringify(results));
// Object.assign(this, {test});
}
);
}
}
However when I run the above code I get in JavaScript console the error:
ERROR TypeError: Object(...) is not a function
at BarVerticalComponent../src/common/base-chart.component.ts.BaseChartComponent.bindWindowResizeEvent (index.js:7818)
at BarVerticalComponent../src/common/base-chart.component.ts.BaseChartComponent.ngAfterViewInit (index.js:7730)
at callProviderLifecycles (core.js:12689)
at callElementProvidersLifecycles (core.js:12656)
at callLifecycleHooksChildrenFirst (core.js:12639)
at checkAndUpdateView (core.js:13794)
at callViewAction (core.js:14136)
at execComponentViewsAction (core.js:14068)
at checkAndUpdateView (core.js:13791)
at callViewAction (core.js:14136)
My Html Template File:
<div>
lalalal <br/>
ante pali... <br/>
kala ti na pw... <br/>
Gamiete pali... <br/>
<ngx-charts-bar-vertical
[view]="view"
[scheme]="colorScheme"
[results]="data"
[gradient]="gradient"
[xAxis]="showXAxis"
[yAxis]="showYAxis"
[legend]="showLegend"
[showXAxisLabel]="showXAxisLabel"
[showYAxisLabel]="showYAxisLabel"
[xAxisLabel]="xAxisLabel"
[yAxisLabel]="yAxisLabel"
(select)="onSelect($event)">
</ngx-charts-bar-vertical>
</div>
While the service for retreiving the values is:
import {Injectable} from '#angular/core';
import {HttpClient} from '#angular/common/http';
import {catchError} from 'rxjs/operators';
import {ErrorHandler} from '../shared/lib/error-handler';
import {Observable} from 'rxjs/Observable';
#Injectable()
export class StatisticsService {
constructor(private http: HttpClient) {
}
public getIncidentsByDay(): Observable<any> {
console.log("FEtching Incidents All By Day");
const url = 'statistics/incidents/day';
return this.http.get(url)
.pipe(catchError(ErrorHandler.handleError));
}
}
What am I doing wrong?
I am using Angular version 5.3 and ngx-charts 8.0 which is compatible with Angular 6 and not Angular 5. I installed ngx-charts version 7.4 and everything works fine.
I fixed the problem for me by downgrading to version 7.3.0
yarn add #swimlane/ngx-charts#7.3.0
I think I see the same with ngx-charts-bar-horizontal, whereas before this was not the case. The documentation page seems to be broken at the moment as well, so I assume the software has recently been updated in a broken way.
If you really need to use the 8.0 version, you can upgrade to angular 6 to solve the problem. Here is how you can do the upgrade from v5 to v6 https://stackoverflow.com/a/49474334
You can also think that the documention page is broken by now but ou can find it here https://swimlane.gitbook.io/ngx-charts/v/docs-test/installing

Aurelia, check when DOM is compiled?

How to check when DOM is compiled and inserted from Aurelia repeat cycle when the model is updated?
I have the following html:
<div clas="parent">
<div class="list-group">
<a repeat.for="$item of treeData">${$item.label}</a>
</div>
</div>
Here I need to know when all <a> tags are listed in the DOM, in order to run jquery scroll plugin on the parent <div> container.
At first load, I do that from the attached() method and all is fine.
When I update the treeData model from a listener, and try to update the jquery scroll plugin, it looks that the DOM is not compiled, so my scroll plugin can not update properly.
If I put timeout with some minimum value like 200ms it works, but I don't think it is a reliable workaround.
So is there a way to solve that?
Thanks!
My View Model:
#customElement('tree-view')
#inject(Element, ViewResources, BindingEngine)
export class TreeView {
#bindable data = [];
#bindable filterFunc = null;
#bindable filter = false;
#bindable selectedItem;
constructor(element, viewResources, bindingEngine) {
this.element = element;
this.viewResources = viewResources;
this.bindingEngine = bindingEngine;
}
bind(bindingContext, overrideContext) {
this.dataPropertySubscription = this.bindingEngine
.propertyObserver(this, 'data')
.subscribe((newItems, oldItems) => {
this.dataCollectionSubscription.dispose();
this._subscribeToDataCollectionChanges();
this.refresh();
});
this.refresh();
if (this.filter === true) {
this.filterChanged(this.filter);
}
if (this.selectedItem) {
this.selectedItemChanged(this.selectedItem);
}
}
attached() {
$(this.element).perfectScrollbar();
}
refresh() {
this.treeData = processData(this.data, this.filterFunc);
this.listItemMap = new WeakMap();
this.treeData.forEach(li => this.listItemMap.set(li.item, li));
this.filterChanged(this.filter);
$(this.element).perfectScrollbar('update');
}
This is only part of the code, but most valuable I think.
I attach the jq plugin in attached function and try to update it in refresh function. In general I have listener that track model in other view, which then update that one without triggering bind method.
An approach would be to use something called window.requestAnimationFrame (https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
In your view-model, when you modify your treeData array, try calling
window.requestAnimationFrame(()=>{
$.fn.somePlugin();
});
Haven't tested this out, but based off what you're telling me, this might do what you need.
You could push your code onto the microTaskQueue, which will schedule your function to be executed on the next event loop. For instance:
import { TaskQueue } from 'aurelia-task-queue';
//...
#inject(Element, ViewResources, BindingEngine, TaskQueue)
export class TreeView {
constructor(element, viewResources, bindingEngine, taskQueue) {
this.element = element;
this.viewResources = viewResources;
this.bindingEngine = bindingEngine;
this.taskQueue = taskQueue;
}
refresh() {
this.treeData = processData(this.data, this.filterFunc);
this.listItemMap = new WeakMap();
this.treeData.forEach(li => this.listItemMap.set(li.item, li));
this.filterChanged(this.filter);
// queue another task, which will execute after the tasks queued above ^^^
this.taskQueue.queueMicroTask(() => {
$(this.element).perfectScrollbar('update');
});
}
}