Call component method when model property changes - reactjs

In my Fable app with Elmish I have a view that uses react-slick and a button that should be able to change the slide number on click:
Fable.Import.Slick.slider
[ InitialSlide model.SlideNumber
AfterChange (SlideTo >> dispatch) ]
children
Button.button
[ Button.OnClick (fun _ev -> dispatch (SlideTo 5)) ]
[ str "Go to slide 5" ]
The react component for the slider is defined by react-slick.
The fable wrapper I had to write on my own, so it's not complete because I only defined the properties I need.
module Fable.Import.Slick
open Fable.Core
open Fable.Core.JsInterop
open Fable.Helpers
open Fable.Helpers.React.Props
open Fable.Import.React
type SliderProps =
| InitialSlide of int
| AfterChange of (int -> unit)
interface IHTMLProp
let slickStyles = importAll<obj> "slick-carousel/slick/slick.scss"
let slickThemeStyles = importAll<obj> "slick-carousel/slick/slick-theme.scss"
let Slider = importDefault<ComponentClass<obj>> "react-slick/lib/slider"
let slider (b: IHTMLProp list) c = React.from Slider (keyValueList CaseRules.LowerFirst b) c
So while react-slick defines a property InitialSlide to set the slide that should initially be shown, there's no property to update the slide afterwards. There is a method slickGoTo that should do what I want. But I don't know how or where to call that method while still being compliant with Elmish.
I could imagine that I have to extend the react component and listen to the model property and then call slickGoTo whenever that property changes. But I don't know if that's possible.
So my question is: How can I have a button that changes the slide number on click using slickGoTo that is defined by the component while not hacking the Elmish architecture?

An alternative to storing the reference to the slider object in the model is to use a mutable variable:
let mutable sliderRef : Browser.Element option = None
let gotoSlide n =
match sliderRef with None ->
| None -> ()
| Some slider -> slider?slickGoTo n
the rest is similar:
type Msg =
| CurrentSlide of int
| GotoSlide of int
...
let update msg model =
match msg with
| CurrentSlide n -> { model with slideNumber = n } , []
| GotoSlide n -> gotoSlide n ; model, []
Create your slider like this:
slider
[ Ref (fun slider = sliderRef <- Some slider)
AfterChange (CurrentSlide >> dispatch)
InitialSlide 0
]
slides

To be able to call the method slickGoTo you need to get a reference to the slider object. To do that use the Ref prop that expects a function of type (Browser.Element -> unit). It gets called once. Save that element somewhere, perhaps in your model:
type Model = {
...
slideNumber : int
sliderRef : Browser.Element option
...
}
let gotoSlide sliderRef n =
match sliderRef with None ->
| None -> ()
| Some slider -> slider?slickGoTo n
That way you can call gotoSlide from your update function.
type Msg =
| SetSliderRef of Browser.Element
| CurrentSlide of int
| GotoSlide of int
...
let update msg model =
match msg with
| SetSliderRef e -> { model with sliderRef = Some e } , []
| CurrentSlide n -> { model with slideNumber = n } , []
| GotoSlide n -> gotoSlide model.sliderRef n ; model, []
...
Create your slide like this:
slider
[ Ref (SetSliderRef >> dispatch)
AfterChange (CurrentSlide >> dispatch)
InitialSlide 0
]
slides
I have not tested any of this, so take with a grain of salt.

I just found that within React you previously solved such things using componentWillReceiveProps - that sounds a lot like what I want IMO. But this is now obsolete and there are two recommendations based on my usage:
If you used componentWillReceiveProps to “reset” some state when a prop changes, consider either making a component fully controlled or fully uncontrolled with a key instead.
I don't think I can implement the Fully controlled version (this has to be done by the author of the component, right?), but the fully uncontrolled with a key seems to work. So my slider view now looks like this:
Fable.Import.Slick.slider
[ InitialSlide model.SlideNumber
AfterChange (SlideTo >> dispatch)
Key (string model.SlideNumber) ]
According to the docs everytime the Key changes, the component is recreated. Now this sounds like a lot of overhead and has its own drawbacks (animating the slide doesn't work) but currently it's the simplest solution. Any concerns?

Related

How to extend enum for Typescript in React

I'm trying to figure out how to extend my types to allow a new value.
I want to pass through the type of inventory I have (window, door, or brick). In a schema I fetch from the backend I access
export enum Type {
Window = 'window',
Door = 'door',
/**
* Used to indicate that extending this enum is considered a non-breaking
* change.
*/
Other = '##other',
}
later when I pass the value in as a prop I have inventoryType={Collections.Inventory.Type.Window} for one component, inventoryType={Collections.Inventory.Type.Door} for the second, and I want to be able to pass a new value inventoryType={NewInventoryType.Brick} to my third component.
How do I extend my original enum to include Brick?
Here are a couple of things I tried:
const BRICK_KEY = 'brick'
enum BrickType {
'Brick' = BRICK_KEY
}
export type NewInventoryType = Collections.Inventory.Type.Window | Collections.Inventory.Type.Door | typeof BrickType
Also tried making a const
export const NEW_INVENTORY_TYPES: Record<
| Exclude<Collections.Inventory.Type, Collections.Inventory.Type.Other>
| typeof BRICK_KEY,
string
> = {
[Collections.Inventory.Type.Window]:
INVENTORY_TYPE_LABELS[Collections.Inventory.Type.Window],
[Collections.Inventory.Type.Door]:
INVENTORY_TYPE_LABELS[Collections.Inventory.Type.Door],
Brick: 'Brick',
};
However with the second approach if I try to do a check for inventoryType: typeof NEW_INVENTORY_TYPE I get
This condition will always return 'false' since the types 'Record<Type.Window | Type.Door | "Brick", string>' and 'Type' have no overlap.
I think I'm missing something about Typescript enums here and feel a bit like I'm stumbling around in the dark. Can someone help me extend the enum type?
You can extend enums with this:
enum Type {
Window = 'window',
Door = 'door',
Other = '##other',
}
enum newType {
Brick = "brick"
}
const lastType = { ...Type, ...newType }
console.log(lastType.Window == Type.Window) // true
console.log(lastType.Window == 'window') // true
console.log(lastType.Window == Type['Window']) // true
// and so on

Highcharts Treemap can't update data properly on React JS

I'm having difficulties with Treemap on Higcharts. Currently I'm getting my data with Redux and loading it into Treemap's options with no problem. The original data is an array with 10 items.
The problem occurs when I click on a node, and I do a dispatch to get new data and update the Treemap, if the new data comes with 10 items it will load no problem (just like the parent). But if it has a different length it shows me the following error
TypeError: Cannot read property 'mouseover' of undefined
createChart();
53 | } else {
54 | if (props.allowChartUpdate !== false) {
> 55 | if (!props.immutable && chartRef.current) {
| ^ 56 | chartRef.current.update(
57 | props.options,
58 | ...(props.updateArgs || [true, true])
I can't seem to find what can be.
EDIT: To illustrate more my problem, here's part of my code. I have a main container, a Highcharts component which receive the data as props, a Highcharts custom hook that creates the proper series data based on a Redux Payload.
Main Container has the Treemap container that gets chartSerie props which contains the data to load.
<GridWrapper item xs={12} md={10}>
<TreemapChart serie={chartSerie} idEntity={6927} year={yearsValue} />
/GridWrapper>
The Treemap component initially loads the Treemap perfect, and has this function to dispatch a Redux action and load new data.
events: {
click: function (event) {
dispatch(onTradedSegmentsThunk(idEntity, year, event.point.node.parent))
}
}
The useTreemap hook gets the payload data from the Redux state and creates the proper structure to pass it as chartSerie
const selectedData = segment.length >= 1 ? segment : data
const sortedArray = selectedData.sort((a, b) => segment.length >= 1 ? a.amountFamily < b.amountFamily : a.amountSegment < b.amountSegment)
const newArray = sortedArray.map((item, index) => (
{
id: AlphaId[index],
name: segment.length >= 1 ? item.family : item.segment,
parent: index + 1,
realValue: segment.length >= 1 ? item.amountFamily : item.amountSegment,
value: serieValue[index]
}
))
setSerie(baseSerie.concat(newArray))
}, [data, segment])
return { serie }
What's weird to me is the 'mouseover' property which can't seem to find out why the error mentions it. But I think the problem is that the new data as different length as the first one that gets rendered.

Typing property of a variable which possibly has empty-object type

Background
I'm using React and TypeScript and trying to add an optional parameter to a component. Then I run into a typescript error (ts:2339).
It seems that, when no props is passed, React will pass props={} instead of props=null or props=undefined. So the functional component will receive {}(an empty object), and I cannot change that.
Problem
How should I let TypeScript read the real optional parameters with pre-defined values?
const f = (param: {} | { k: string } = { k: 'v' }) => {
var o
// test 1 TS error: Property 'k' does not exist on type '{} | { k: string; }'. Property 'k' does not exist on type '{}'.(2339)
// o = (param)?.k
// test 2, nullity test. Same error as test1
// if (Object.entries(param).length === 0) param = { k: 'v' };
// o = (param)?.k
// test 3, trying to avoid optional param assignment. Same error as test1
// const _param = param ?? { k: 'v' }
// o = _param.k
// current work-around
o = (param as any)?.k
console.log(o)
}
f()
See the code in TS playground: https://www.typescriptlang.org/play?ssl=18&ssc=4&pln=1&pc=1#code/MYewdgzgLgBAZjAvDAFABwIYCcMFsBcMA3gL4wA+xMA1odFgJZgDmMZyRNhA5AG7dsAlEgB8xAFAwpMXthghJ0gPRKYUAKbQYARhgAVAMox1WLCCyEACmbQmoATxjdqAgCYhNMMCFjqAHgxa4Gr2tk6kFFS0MPRMzADcbNwAdFLWILZYDk4uMO6e3r4BQWAhYdykKSgATADMtQCcgopSKvJIqJg4uIIA-MnUMOItMG0aWtUANF4ArgA2cwzZ41CpBnjqxqbmMBgQappQ2iNtDAgoAPIARgBW6sCr6mBQjJro2HiCyXNPzFAAFkhEMgAAzCLp4DqcaJ8AQkeInVQgDrvbp9AbDaSjVQrGC1aYvexxNTIjC8EAMVzyNBQBjgDBzGAQ3C7CAQBjMMC4J6rGDrblbMxYVkHaDHLFtUCQWAAfWZHXlvV6UR4-DYiPayDlH1wGI1wBmph5MAA7uZqABabAgGZgVwjZHIVGQva7MD2dHUEZSiAgH7fEDMFAgZokcRwFCCIA
The param variable is never empty in this example and always has the property k. Yet I cannot use param?.k or anything I can come up with. I know that the any type works like disabling the TypeScript function. So, is there a way to let TypeScript read the true value of the parameter? Thanks.
What I did
I've read almost all similar questions on Stack Overflow but I see no good answer. Here are some references.
test2: How do I test for an empty JavaScript object?
test3: Set default value of property of object passed on parameter to function
I can't use null instead of {} because of React.
I think this can be helpful but I can't find API on non-route component: React route params with default value .
This one is with the class component, but not resolving my question: Default property value in React component using TypeScript
You can directy destructure the parameter in the function signature with a default value to always get the value:
const f = ({ k = 'v' }: { k?: string }) => {
...
and then do var o = k.
Alternatively you can test if k is a key of param. This can be done with:
if ('k' in param) {
o = param.k
}
but it is more cumbersome.
import { FC } from 'react'
type Props = {
k: string
}
const foo: FC<Props> = (props = { k: 'v' }) => {
const { k } = props // string
return null as any
}
Since you have provided default value for props, you can omit any checkings of k

Problem with a custom Control in Windows Form

I'm trying to write my own MineSweeper in F#.
I create a type of Button :
type myButton(pos : Point, boum : bool, wnd : Form) =
inherit Button(Parent = wnd,
Bounds = Rectangle(Point((pos.X+1) * size,(pos.Y+1) * size), Size(size,size)),
Visible = true,
Text = if boum = true then "B" else "")
let (bomb : bool) = boum
member this.Bomb with get() = bomb
The buttons are in a Form and I can get these buttons with GethildAtPoint. But I get a Control not a myButton so I can't access to the the field Bomb.
How can I get it ? I tried with the Tag property, but without success.
Thank you.
When you call GetChildAtPoint, you will get back a Control, which you can then type test with a pattern match to see if it's your custom button:
let ctrl = form.GetChildAtPoint pt
match ctrl with
| :? myButton as myb ->
let isBomb = myb.Bomb ()
// Do something here
()
| _ -> () // ignore anything else

Angular2 ngOnChanges clone #Input array

I'm using a DashboardComponent that gets the data from my DashboardService. This Component then passes my array of objects to my form component.
(Plunkr link at bottom of post)
DashboardComponent.ts
private bottleArray: Bottle[] = [];
ngOnInit() {
// Get bottle types from service to the form needing them
this.dashboardService.getBottleTypesAndNames()
.subscribe(bottlesData => {
bottlesData.forEach(bottle => {
// Convert to Bottle type
let bottleObject: Bottle = new Bottle(bottle.bottleTypeId, bottle.bottleName);
this.bottleArray.push(bottleObject);
});
});
}
DashboardComponent.html
<ct-create-order-form [bottleArray]="bottleArray"> </ct-create-order-form>
I did it that way so that my form components linked to my Dashboard won't be doing any call to my service.
I'm trying to clone my #Input so that my data updated from the form is not linked to my parent component (Dashboard), but I can't seem to do it... See code below :
CreateOrderFormComponent.ts
export class CreateOrderFormComponent implements OnChanges {
#Input() private bottleArray: Bottle[];
constructor() { }
private clonedBottleArray: BottleCommand[];
ngOnChanges(changes) {
if (changes.bottleArray) {
let test: BottleCommand[] = changes.bottleArray.currentValue;
// Cloning
console.log(test); // Array of 6 Bottles
this.clonedBottleArray = [...test];
console.log(this.clonedBottleArray); // Empty Array
this.clonedBottleArray = Array.from(test);
console.log(this.clonedBottleArray); // Empty Array
this.clonedBottleArray = test.slice();
console.log(this.clonedBottleArray); // Empty Array
this.clonedBottleArray = test;
console.log(this.clonedBottleArray); // Array of 6 bottles
}
}
Is there any way to achieve what I am doing ? I don't understand why I can't clone my Input when I get the data ?
From this Youtube video made by AngularConnect, he is doing the exact same except that he is manipulating an Object, and I'm manipulating an Array of Objets.
https://youtu.be/-nsedZwvl9U?t=12m22s
EDIT : After creating a Plunkr, this seems to be working correctly in there.
https://plnkr.co/edit/js1vl0fcgOKtQNqXsWTL?p=preview
EDIT 2 : At the ngOnInit() from my DashboardComponent, if I mock the data, it is cloned correctly in my child component.
Looks like angular OnChange not firing due to it specific way of checking, here's brief explanation from this answer:
During change detection, when Angular checks components' input properties for change, it uses (essentially) === for dirty checking. For arrays, this means the array references (only) are dirty checked. Since the rawLapsData array reference isn't changing, ngOnChanges() will not be called.
And in your example, you're .pushing bottles in bottleArray, so OnChange doesn't fire on the same array reference.
To get the changes, you could use DoCheck:
ngDoCheck() {
console.log(this.bottleArray);
this.clonedBottleArray = [...this.bottleArray].slice(0, 4);
console.log(this.clonedBottleArray);
}
it will fire when you push new values to the bottleArray. Working plunker here.

Resources