Ionic2 view doesn't bind with observable - data-binding

I have the following component class that receives an observable from a service. I want my component to unsubscribe when destroyed. This is the code is working and the view gets updated (the view renders different content depending on the value of state with *ngIf):
#Component({
templateUrl: 'my-widget.component.html',
selector: 'my-widget'
})
export class MyWidgetComponent implements OnInit, OnDestroy {
// Enum type variable to keep state
state: MyWidgetStateType;
// This is needed to be able to use the enum type in the template
private myWidgetStateType = MyWidgetStateType;
// Keep a reference to the observable to unsubscribe from it
private sub: Subscriber<any>;
constructor(public myWidgetService: MyWidgetService) {
}
ngOnInit(): void {
this.sub = Subscriber.create((state: MyWidgetStateType) => {
console.log('Widget state: ' + MyWidgetStateType[state]);
this.state = state;
});
this.myWidgetService.getState().subscribe(this.sub);
}
ngOnDestroy(): void {
// Unsubscribe here
this.sub.unsubscribe();
}
}
The state type is an enum with 3 states:
export enum MyWidgetStateType {
Enabled,
Dormant,
Locked
};
And the view my-widget.component.html:
<h2>My Widget</h2>
<div *ngIf="state === myWidgetStateType.Enabled">
<p>Enabled</p>
</div>
<div *ngIf="state === myWidgetStateType.Dormant">
<p>Dormant</p>
</div>
<div *ngIf="state === myWidgetStateType.Locked">
<p>Locked</p>
</div>
However, if I try to extract the call in the subscriber to a private function, the console.log method works but the view doesn't render any content according to the state (the <h2> title is rendered but there is no <div> element):
#Component({
templateUrl: 'my-widget.component.html',
selector: 'my-widget'
})
export class MyWidgetComponent implements OnInit, OnDestroy {
// Enum type variable to keep state
state: MyWidgetStateType;
// This is needed to be able to use the enum type in the template
private myWidgetStateType = MyWidgetStateType;
// Keep a reference to the observable to unsubscribe from it
private sub: Subscriber<any>;
constructor(public myWidgetService: MyWidgetService) {
}
ngOnInit(): void {
this.sub = Subscriber.create(this.onStateChange);
this.myWidgetService.getState().subscribe(this.sub);
}
ngOnDestroy(): void {
// Unsubscribe here
this.sub.unsubscribe();
}
private onStateChange(state: MyWidgetStateType): void {
console.log('Widget state: ' + MyWidgetStateType[state]);
this.state = state;
}
}
The question is: why if the onStateChange function gets called, the view never gets the value update?
Of course this is not blocking my work because I have the solution above, but I'm just curious.

Related

how to animate state transitions in Blazor when rendering a list of object with their own lifecycle?

I know that there is a post closely related to my question (How to animate state transitions in Blazor?).
However, my problem is the following : Consider a list of toasts for instance. I want to display 5 toasts, they can be removed by clicking on them, or they remove themselves when their timers is out.
Code simplified as example.
Component Toasts
#inject ToastService ToastService
<div class="toasts">
#foreach (ToastData data in ToastService.List)
{
<Toast Data="data" />
}
</div>
#code {
protected override async Task OnInitializedAsync()
{
ToastService.OnListChange += RefreshList;
}
public void RefreshList()
{
StateHasChanged();
}
}
ToastService
public class ToastService
{
private List<ToastData > _list;
public List<ToastData > List
{
get
{
var list = _list.Take(5).ToList();
foreach (ToastData data in list)
{
data.StartCountDown();
}
return list;
}
}
public event Action OnListChange;
public ToastService()
{
_List = new List<ToastData >();
}
public async Task CreateToast(ToastData data)
{
_list.Add(data);
OnListChange?.Invoke();
}
public async Task RemoveToast(ToastData data)
{
_list.Remove(data);
OnListChange?.Invoke();
}
}
ToastData
public class ToastData
{
private ToastService _toastService;
private bool _isCountdownStarted;
private System.Timers.Timer _countdown;
public ToastData(ToastService toastService)
{
_toastService= toastService;
}
public void StartCountDown()
{
if (_isCountdownStarted)
return;
_countdown = new System.Timers.Timer(5000);
_countdown.AutoReset = false;
_countdown.Start();
_countdown.Elapsed += RemoveNotification;
_isCountdownStarted = true;
}
public void RemoveNotification()
{
_countdown.Close();
_toastService.RemoveNotification(this);
}
private void RemoveNotification(object source, ElapsedEventArgs args)
{
RemoveNotification();
}
}
Component Toast
<div #onclick="Clicked" class="toast">
Some message on a toast
</div>
#code {
[Parameter] public ToastData Data { get; set; }
public void Clicked()
{
Data.RemoveNotification();
}
}
The above example work fine, cause there's no animation yet.
But now, I want to add an animation of the Toast component. So I modify the ToastData to first call a Hide method, this method will notify the Toast component who will add a CSS class that will animate the removal.
This works fine, until this happen :
Toast component 1 start to animate the removal
Toast component 2 start to animate the removal
Toast 1 is removed, the list is refreshed
Toast 2 is now Toast 1, the animation is gone, and suddenly it disappear
Worst even, a Toast 3 would become Toast 2 and will animate even if not intended to be removed yet.
I understand that Blazor choose to reuse HTML, that's why in the Toasts component, all Toast component will always be the same. That's why I put the logic in the ToastData.
I'm guessing I'm missing something...
Any help or insight appreciated!
When rendering components in a loop, the #key directive attribute is your friend.
It will help Blazor keep the relationship between data and a component instance.
https://learn.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-6.0#use-key-to-control-the-preservation-of-elements-and-components

Is it possible to make React-like compound components in lit 2.0?

I tried to build compound components with Lit 2.0 but passing data to slots as attributes seems impossible.
<my-accordion>
<my-accordion-title>Title</my-accordion-title>
<my-accordion-content>Content</my-accordion-content>
</my-accordion
How I can pass "extended" propery to custom elements slots?
Here is my custom elements:
#customElement("my-accordion")
export class MyAccordion extends LitElement {
#property()
extended: boolean = false;
toggleExtend(){
this.extended = !this.extended
}
render() {
return html`
<div #click=${this.toggleExtend}>
<slot .extended=${this.extended}></slot>
</div>
`;
}
}
#customElement("my-accordion-title")
export class MyAccordionTitle extends LitElement {
// want to access parent node extended property here
render() {
return html`
<div>
<slot></slot>
</div>
`;
}
}
To assign the extended property to slotted children, get the slot element's assignedElements() array.
#customElement("my-accordion")
export class MyAccordion extends LitElement {
#property()
extended: boolean = false;
toggleExtend(){
this.extended = !this.extended
}
updated(changed: PropertyValues<this>) {
if (changed.has('extended'))
this.extendedChanged()
}
extendedChanged() {
for (const child of this.slot.assignedElements()) {
if (child instanceof MyAccordionTitle)
child.extended = this.extended;
}
}
#query('slot') slot: HTMLSlotElement | null;
render() {
return html`
<div #click=${this.toggleExtend}>
<slot></slot>
</div>
`;
}
}
#customElement("my-accordion-title")
export class MyAccordionTitle extends LitElement {
// want to access parent node extended property here
render() {
return html`
<div>
<slot></slot>
</div>
`;
}
}
NB: When assigning click listeners to <div> and other non-interactive elements, there are many accessibility issues involved. It's usually recommended therefore to use a <button> element, or in your case maybe a <details> and <summary>

How do I make my boolean column in ag-grid show checkboxes

I have a boolean column in my grid which is currently displaying 'true' or 'false' but I want it to show a checkbox instead.
How should I do this.
We are using ag-grid 25 with Angular and Adaptable.
You can write your own cell renderer that renders a checkbox instead of a string. Below is an example:
import { Component, OnDestroy } from '#angular/core';
import { ICellRendererAngularComp } from '#ag-grid-community/angular';
#Component({
selector: 'checkbox-renderer',
template: `
<input
type="checkbox"
(click)="checkedHandler($event)"
[checked]="params.value"
/>
`,
})
export class CheckboxRenderer implements ICellRendererAngularComp, OnDestroy {
private params: any;
agInit(params: any): void {
this.params = params;
}
checkedHandler(event) {
let checked = event.target.checked;
let colId = this.params.column.colId;
this.params.node.setDataValue(colId, checked);
}
}
import { CheckboxRenderer } from "./checkbox-renderer.component";
this.frameworkComponents = {
checkboxRenderer: CheckboxRenderer
};
Live Demo
Resource
https://blog.ag-grid.com/binding-boolean-values-to-checkboxes-in-ag-grid
As you say that you are using AdapTable then you can just add the name of the column to the CheckBoxColumns list in PredefinedConfig / UserInterface
See: https://docs.adaptabletools.com/docs/predefined-config/user-interface-config#checkboxcolumns
So something like:
export default {
UserInterface: {
CheckboxColumns: ['myBooleanColumn'],
},
} as PredefinedConfig;
That will dynamically create a CellRenderer very similar to the one which NearHuscarl suggests.
If the column is ReadOnly the checkboxes will be disabled.
Its worth noting that the CheckboxColumnClickedEvent will fire each time a cell in the column is clicked.

Flutter State Management Examples

In a complex app, sometimes a Global Variable 'attached' to a widget, can be changed by some 'EXTERNAL EVENT' such as (1) A timer that run in another thread, or (2) socket.io server emit event (3) Others ......
Let's call this global variable gintCount and the app has 3 pages, namely:
Page 1: A 'Dynamic' page that need to display the latest value of gintCount.
Page 2: Another 'Dynamic' page that need to display the latest value of gintCount, with a Text Input Field.
Page 3: A 'Static' page that do nothing when gintCount changes.
Suppose the user is doing something in Page 1 or Page 2, when and where should we 'Refresh' the page to display the latest value that may/might be changed by EXTERNAL event?
I read the other Q&A in Stack Overflow and it is said that there are 4 ways for the State Management of Flutter, they are namely:
Using setState
Using ScopedModal
Using Rxdart with BLoC
Using Redux
Since I'm a newbie in Flutter, I am completely lost in 2 to 4, so I've build an app using no. 1, i.e. setState. to demonstrate how we can manage states in flutter. And I hope, in the future, I am able to (or somebody else) provide answers by using no. 2 to 4.
Let's take a look at the running app in the following animation gif:
Screen Shot Gif Link
As you can see in the gif, there is a Global Counter in Page 1 and Page 2, and Page 3 is a static Page.
Let me explain how I did it:
The complete source code can be found at the following address:
https://github.com/lhcdims/statemanagement01
There are 7 dart files, they are namely:
gv.dart: Stores all the Global Variables.
ScreenVariable.dart: Get the height/width/font size of screen etc. You may ignore this.
BottomBar.dart: The bottom navigation bar.
main.dart: The main program.
Page1.dart: Page 1 widget.
Page2.dart: Page 2 widget.
Page3.dart: Page 3 widget.
Let's first take a look at gv.dart:
import 'package:flutter/material.dart';
class gv {
static var gstrCurPage = 'page1'; // gstrCurPage stores the Current Page to be loaded
static var gintBottomIndex = 0; // Which Tab is selected in the Bottom Navigator Bar
static var gintCount = 0; // The Global Counter
static var gintCountLast = 0; // Check whether Global Counter has been changed
static var gintPage1Counter = 0; // No. of initState called in Page 1
static var gintPage2Counter = 0; // No. of initState called in Page 2
static var gintPage3Counter = 0; // No. of initState called in Page 3
static bool gbolNavigatorBeingPushed = false; // Since Navigator.push will called the initState TWICE, this variable make sure the initState only be called once effectively!
static var gctlPage2Text = TextEditingController(); // Controller for the text field in Page 2
}
How did I simulate an External Event that changes the global variable gv.gintCount?
Ok, I create a thread in main.dart that runs the timer 'funTimerExternal', and increment gv.gintCount every second!
Now, let's take a look at main.dart:
// This example tries to demonstrate how to maintain the state of widgets when
// variables are changed by External Event
// e.g. by a timer of another thread, or by socket.io
// This example uses setState and a timer to maintain States of Multiple Pages
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "package:threading/threading.dart";
import 'gv.dart';
import 'Page1.dart';
import 'Page2.dart';
import 'Page3.dart';
import 'ScreenVariables.dart';
void main() { // Main Program
var threadExternal = new Thread(funTimerExternal); // Create a new thread to simulate an External Event that changes a global variable defined in gv.dart
threadExternal.start();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
sv.Init(); // Init Screen Variables
runApp(new MyApp()); // Run MainApp
});
}
void funTimerExternal() async { // The following function simulates an External Event e.g. a global variable is changed by socket.io and see how all widgets react with this global variable
while (true) {
await Thread.sleep(1000);
gv.gintCount += 1;
}
}
class MyApp extends StatefulWidget { // Main App
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
initState() {
super.initState();
var threadTimerDefault = new Thread(funTimerDefault); // *** Set funTimerDefault, to listen to change of Vars ***
threadTimerDefault.start();
}
void funTimerDefault() async {
while (true) {
await Thread.sleep(500); // Allow this thread to run each XXX milliseconds
if (gv.gintCount != gv.gintCountLast) { // Check any changes need to setState here, if anything changes, setState according to gv.gstrCurPage
gv.gintCountLast = gv.gintCount;
switch (gv.gstrCurPage) {
case 'page1':
setState(() {}); // Page 1: Refresh Page
break;
case 'page2':
setState(() {}); // Page 2: Refresh Page
break;
default: // Page 3: Do Nothing, since Page 3 is static
break;
}
}
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // Disable Show Debug
home: MainBody(),
);
}
}
class MainBody extends StatefulWidget {
#override
_MainBodyState createState() => _MainBodyState();
}
class _MainBodyState extends State<MainBody> {
#override
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
switch (gv.gstrCurPage) { // Here Return Page According to gv.gstrCurPage
case 'page1':
return ClsPage1();
break;
case 'page2':
return ClsPage2();
break;
default:
return ClsPage3();
break;
}
return ClsPage1(); // The following code will never be run, to avoid warning only
}
}
As you can see, I use another timer 'funTimerDefault' to keep track of changes in gv.gintCount, and determine whether setState should be called every XXX milliseconds. (XXX is currently set at 500)
I know, this is stupid!
How can I create similar examples by using ScopedModal, or Rxdart with BLoC, or Redux?
Before anyone provides any answers, please bear in mind that the Global Variable gintCount, is not changed by ANY USER INTERACTION, but an EXTERNAL EVENT that IS NOT PART OF ANY WIDGETS. For example, you can regard this app as:
A CHAT app, that 'gintCount' is a message sent to you by someone else thru socket.io server. Or,
A Multi-Player On-line Game, that 'gintCount' is the position of another player in YOUR SCREEN, which is controlled by that player using another Mobile Phone!
For your need, you should definitely look more into the architectures available, that you talked about.
For example, REDUX matches exactly what you need to solve your issue.
I can only advise you to take a look at this presentation of REDUX :
https://www.youtube.com/watch?v=zKXz3pUkw9A
It is very understandable even for newbies of this pattern (which I was not so long ago).
When you've done that, take a look at http://fluttersamples.com/
This website contains example projects for a dozen of different patterns. That may help you get started
I've rewritten the example using Redux, let's take a look at the screen cap:
As you can see, there are 2 counters in Page 1, the variables are stored in gv.dart
In gv.dart (The dart file that stores all Global Variables), I created a 'Store':
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'dart:convert';
enum Actions { Increment } // The reducer, which takes the previous count and increments it in response to an Increment action.
int counterReducer(int intSomeInteger, dynamic action) {
if (action == Actions.Increment) {
// print('Store Incremented: ' + (intSomeInteger + 1).toString());
return intSomeInteger + 1;
}
return intSomeInteger;
}
class gv {
static Store<int> storeState = new Store<int>(counterReducer, initialState: 0);
static var gstrCurPage = 'page1'; // gstrCurPage stores the Current Page to be loaded
static var gintBottomIndex = 0; // Which Tab is selected in the Bottom Navigator Bar
static var gintGlobal1 = 0; // Global Counter 1
static var gintGlobal2 = 0; // Global Counter 2
static var gintPage1Counter = 0; // No. of initState called in Page 1
static var gintPage2Counter = 0; // No. of initState called in Page 2
static var gintPage3Counter = 0; // No. of initState called in Page 3
static bool gbolNavigatorBeingPushed = false; // Since Navigator.push will called the initState TWICE, this variable make sure the initState only be called once effectively!
static var gctlPage2Text = TextEditingController(); // Controller for the text field in Page 2
}
Again, in main.dart, I created another thread 'funTimerExternal' to simulate an 'External Event' that some global variables are changed by, say, socket.io server emit event.
At the end of 'funTimerExternal', after some variables are changed, I called:
gv.storeState.dispatch(Actions.Increment);
to change the state of Page1 OR Page2, IF AND ONLY IF the user is navigating Page 1 or Page 2. (i.e. do nothing when user is navigating Page 3)
main.dart :
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:threading/threading.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'gv.dart';
import 'Page1.dart';
import 'Page2.dart';
import 'Page3.dart';
import 'ScreenVariables.dart';
void main() { // Main Program
var threadExternal = new Thread(
funTimerExternal); // Create a new thread to simulate an External Event that changes a global variable defined in gv.dart
threadExternal.start();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
sv.Init(); // Init Screen Variables
runApp(new MyApp()); // Run MainApp
});
}
void funTimerExternal() async { // The following function simulates an External Event e.g. a global variable is changed by socket.io and see how all widgets react with this global variable
while (true) {
await Thread.sleep(1000);
gv.gintGlobal1 += 1;
gv.gintGlobal2 = (gv.gintGlobal1 / 2).toInt();
gv.storeState.dispatch(Actions.Increment);
}
}
class MyApp extends StatefulWidget { // Main App
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return StoreProvider(
store: gv.storeState,
child: MaterialApp(
debugShowCheckedModeBanner: false, // Disable Show Debug
home: MainBody(),
),
);
}
}
class MainBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
switch (gv.gstrCurPage) {
// Here Return Page According to gv.gstrCurPage
case 'page1':
gv.gintPage1Counter += 1;
return StoreConnector<int, int>(
builder: (BuildContext context, int intTemp) {
return new ClsPage1(intTemp);
}, converter: (Store<int> sintTemp) {
return sintTemp.state;
},);
break;
case 'page2':
gv.gintPage2Counter += 1;
return StoreConnector<int, int>(
builder: (BuildContext context, int intTemp) {
return new ClsPage2(intTemp);
}, converter: (Store<int> sintTemp) {
return sintTemp.state;
},);
break;
default:
return ClsPage3();
break;
}
}
}
Unlike the example provided on the web, the 'Store' is not declared inside main.dart, but inside another dart file gv.dart. i.e. I separated the UI and data!
The complete example can be found here:
https://github.com/lhcdims/statemanagement02
Thanks again for the help of Miiite and shadowsheep.

How to make PrimeNG Dropdown Keyboard Accessible

I want to make dropdown keyboard accessible. Right now, its not working when i am using keyboard up and down arrow. I applied tabindex but still not working. Anyone have any idea about this..
<p-dropdown [options]="cities" tabindex="1" placeholder="Select an option"></p-dropdown
I tried this it working properly using keyboard up and down arrow :
npm version :
"primeng": "^4.1.3"
in html file
<p-dropdown [options]="cities" [(ngModel)]="selectedCity" tabindex="1" placeholder="Select an option"></p-dropdown>
module.ts
import { DropdownModule } from 'primeng/primeng';
#NgModule({
imports: [
DropdownModule
]
})
component.ts
import {SelectItem} from 'primeng/primeng';
cities: SelectItem[];
selectedCity: string;
PrimeNG not support reading dropdown options by default. Default behavior is navigation by arrows and change value. Default tabulator key action is hide overlay with element items. My solution forces tabulator key to choose dropdown option and when you hit enter key the value is changed.
I prepared angular directive which override default dropdown behavior. You can add add this directive do module and import it in place where you want use this change.
Testing for PrimeNG 8.0.0:
#Directive({
selector: 'p-dropdown',
})
export class DropdownDirective implements OnInit, OnDestroy {
readonly KEY_DOWN_EVENT: string = 'keydown';
readonly FOCUS_IN_EVENT: string = 'focusin';
readonly TABINDEX_ATTRIBUTE: string = 'tabindex';
readonly LIST_ITEM_SELECTOR: string = 'li';
private focusInSubscription: Subscription = new Subscription();
private subscriptions: Subscription = new Subscription();
private listElementSubscriptions: Subscription[] = [];
private readonly dropdownHtmlElement: HTMLElement;
constructor(private dropdown: Dropdown,
private elementRef: ElementRef) {
this.dropdownHtmlElement = this.elementRef.nativeElement as HTMLElement;
}
ngOnInit(): void {
this.replaceKeyDownAction();
this.subscribeToDropdownShowEvent();
this.subscribeToDropdownHideEvent();
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe();
}
private subscribeToDropdownShowEvent() {
this.subscriptions.add(
this.dropdown.onShow.subscribe(() => {
this.updateElementsList();
this.subscribeToFocusInEvent();
}),
);
}
private subscribeToDropdownHideEvent() {
this.subscriptions.add(
this.dropdown.onHide.subscribe(() => {
this.unsubscribeFromFocusInEvent();
this.unsubscribeFromListElementsKeyDownEvents();
}),
);
}
private updateElementsList() {
const listElements = this.dropdownHtmlElement.querySelectorAll<HTMLLIElement>(this.LIST_ITEM_SELECTOR);
listElements.forEach((listElement: HTMLLIElement) => {
this.subscribeToListElementKeyDownEvent(listElement);
listElement.setAttribute(this.TABINDEX_ATTRIBUTE, '0');
});
}
private subscribeToListElementKeyDownEvent(listElement: HTMLLIElement) {
this.listElementSubscriptions.push(
fromEvent(listElement, this.KEY_DOWN_EVENT)
.pipe(filter((event: KeyboardEvent) => event.key === KEYBOARD_KEY.ENTER))
.subscribe(() => {
// Simulation of mouse click of list element (trigger with (click) event in p-dropdownItem component which is child element of p-dropdown)
listElement.click();
}),
);
}
private unsubscribeFromListElementsKeyDownEvents() {
this.listElementSubscriptions.forEach((singleSubscription: Subscription) => singleSubscription.unsubscribe());
this.listElementSubscriptions = [];
}
private subscribeToFocusInEvent() {
this.focusInSubscription = fromEvent(document, this.FOCUS_IN_EVENT).subscribe(({target}) => {
// Situation when focus element is outside dropdown component
if (!this.dropdownHtmlElement.contains(target as HTMLElement)) {
this.dropdown.hide();
}
});
}
private unsubscribeFromFocusInEvent() {
this.focusInSubscription.unsubscribe();
}
/**
* Overwrite default onKeydown method from PrimeNG dropdown component
*/
private replaceKeyDownAction() {
const onKeyDownOriginFn = this.dropdown.onKeydown.bind(this.dropdown);
this.dropdown.onKeydown = (event: KeyboardEvent, search: boolean) => {
if (event.which === 9) {
// Napisuję domyślne zachowanie tabulatora zdefiniowanego w klasie komponentu Dropdown z biblioteki PrimeNG
} else {
onKeyDownOriginFn(event, search);
}
}
}
}

Resources