Explore all State Container open source software, libraries, packages, source code, cloud functions and APIs.

Popular New Releases in State Container

redux

v4.2.0

vuex

v4.0.2

mobx

mobx@6.5.0

redux-devtools

react-json-tree@0.16.2

react-native-debugger

v0.12.1

Popular Libraries in State Container

redux

by reduxjs doticontypescriptdoticon

star image 57862 doticonNOASSERTION

Predictable state container for JavaScript apps

vuex

by vuejs doticonjavascriptdoticon

star image 27468 doticonMIT

🗃️ Centralized State Management for Vue.js.

mobx

by mobxjs doticontypescriptdoticon

star image 25031 doticonMIT

Simple, scalable state management.

redux-saga

by redux-saga doticonjavascriptdoticon

star image 22138 doticonNOASSERTION

An alternative side effect model for Redux apps

mpvue

by Meituan-Dianping doticonjavascriptdoticon

star image 20382 doticonMIT

基于 Vue.js 的小程序开发框架,从底层支持 Vue.js 语法和构建工具体系。

js-stack-from-scratch

by verekia doticonjavascriptdoticon

star image 18568 doticonMIT

🛠️⚡ Step-by-step tutorial to build a modern JavaScript stack.

redux-thunk

by reduxjs doticonjavascriptdoticon

star image 16590 doticonMIT

Thunk middleware for Redux

fks

by JacksonTian doticonjavascriptdoticon

star image 16584 doticon

前端技能汇总 Frontend Knowledge Structure

dva

by dvajs doticonjavascriptdoticon

star image 15740 doticonMIT

🌱 React and redux based, lightweight and elm-style framework. (Inspired by elm and choo)

Trending New libraries in State Container

vue-demi

by vueuse doticonjavascriptdoticon

star image 1514 doticonMIT

🎩 Creates Universal Library for Vue 2 & 3

unplugin-vue-components

by antfu doticontypescriptdoticon

star image 1451 doticonMIT

📲 On-demand components auto importing for Vue

PPet

by zenghongtu doticontypescriptdoticon

star image 1415 doticonMIT

👻在你的桌面放一个萌妹子,多一点趣味😏~(支持Mac、Win和Linux)

vue3-antd-admin

by buqiyuan doticontypescriptdoticon

star image 893 doticonMIT

基于vue-cli/vite + vue3.0 + ant-design-vue2.0 + typescript hooks 的基础后台管理系统模板 RBAC的权限系统, JSON Schema动态表单,动态表格,漂亮锁屏界面

vue-i18n-next

by intlify doticontypescriptdoticon

star image 781 doticonMIT

Vue I18n for Vue 3

newbee-mall-api

by newbee-ltd doticonjavadoticon

star image 725 doticonGPL-3.0

🔥 🎉新蜂商城前后端分离版本-后端API源码

cool-admin-vue

by cool-team-official doticoncssdoticon

star image 680 doticonMIT

cool-admin一个很酷的后台权限管理框架,模块化、插件化、CRUD极速开发,永久开源免费,基于midway.js 2.0、typeorm、mysql、jwt、element-ui、vuex、vue-router、vue等构建

taro-mall

by jiechud doticonjavascriptdoticon

star image 595 doticon

Taro_Mall是一款多端开源在线商城应用程序,后台是基于litemall基础上进行开发,前端采用Taro框架编写,现已全部完成小程序和h5移动端,后续会对APP,淘宝,头条,百度小程序进行适配。Taro_Mall已经完成了 litemall 前端的所有功能

rtk-query

by rtk-incubator doticontypescriptdoticon

star image 580 doticonMIT

Data fetching and caching addon for Redux Toolkit

Top Authors in State Container

1

ktsn

13 Libraries

star icon2610

2

PacktPublishing

13 Libraries

star icon399

3

zalmoxisus

12 Libraries

star icon1756

4

mobxjs

11 Libraries

star icon36499

5

reduxjs

10 Libraries

star icon97579

6

GeekyAnts

10 Libraries

star icon284

7

egoist

9 Libraries

star icon3958

8

eddyerburgh

9 Libraries

star icon290

9

infinitered

9 Libraries

star icon704

10

diegothucao

8 Libraries

star icon65

1

13 Libraries

star icon2610

2

13 Libraries

star icon399

3

12 Libraries

star icon1756

4

11 Libraries

star icon36499

5

10 Libraries

star icon97579

6

10 Libraries

star icon284

7

9 Libraries

star icon3958

8

9 Libraries

star icon290

9

9 Libraries

star icon704

10

8 Libraries

star icon65

Trending Kits in State Container

No Trending Kits are available at this moment for State Container

Trending Discussions on State Container

How to modularize this react state container?

How we can call a method from child with State container Blazor Webassembly

Bounded value not updated when modified by JsInterop

Blazor components: How to communicate from grandchild to child to parent or grandchild to parent

Is there a way to trigger two consecutive events in Blazor with a single click?

Flatlist undefined is not an object React-native

How to use container exits immediately after startup in docker?

How to append data to existing data object

Blazor server side: refresh gui of after api call

A fast way to fill contours with the ability to export to a polygon-like format

QUESTION

How to modularize this react state container?

Asked 2021-Sep-22 at 13:32

So at work we have this awesome state container hook we built to use in our React application and associated packages. First a little background on this hook and what I'd like to preserve before getting to what I want to do with it. Here's the working code. You'll notice it's commented for easy copy and paste to create new ones.

1// Set this accordingly.
2const hookName = "MyState";
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify "null" if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () => ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) => {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) => ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from "react";
41import _cloneDeep from "lodash/cloneDeep";
42
43const Context = React.createContext<
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48>(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) => reducerDispatch({type, data} as Action);
60    return (
61        <Context.Provider value={{ state, dispatch }}>
62            {children}
63        </Context.Provider>
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = <A extends Actions, T extends keyof A, D extends A[T]>(type: T, ...data: (D extends null ? [] : [D])) => void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) => Partial<State> | void : (state: State, data: Actions[K]) => Partial<State> | void;
85};
86

What's cool about it is that the state and dispatch functions are very rigidly typed.

  • The type of initialState is inferred from its structure so when copying this code to make a new state container hook you don't have to write a State type and then also a default state object.
  • The Actions type defines actions that are available to be dispatched and the type of each key is the type that is used to enforce the second argument to our dispatch function.
  • The actionHandlers object must have a handler for each action defined on Actions and the handler functions have to conform exactly, receiving the state object as the first argument, and the second argument being a data object that matches the relevant type value on Actions.
  • The dispatch function that consumers of this hook use is almost impossible to misuse. The first argument only accepts valid actions from Actions keys and the second argument is either not allowed in the case of the Actions type being null or is forced to conform to the Actions type specified.

Here's a typescript playground if you wanna mess with it (go to line 65 to tinker with making dispatch calls)

Okay so what you might notice is that the bottom half of the code snippet is boilerplate. Well we use these hooks a lot so we are copying and pasting them into various modules in our project. It works great but it's not very DRY and any change to the boilerplate obviously has to be propagated to every single hook. Tedious.

I want to modularize the boilerplate bit but as I start trying to think about it my mind gets boggled about how to maintain the rigid type structures behind the scenes. Here's a brainstorm of what it might be like to consume such a module:

1// Set this accordingly.
2const hookName = "MyState";
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify "null" if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () => ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) => {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) => ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from "react";
41import _cloneDeep from "lodash/cloneDeep";
42
43const Context = React.createContext<
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48>(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) => reducerDispatch({type, data} as Action);
60    return (
61        <Context.Provider value={{ state, dispatch }}>
62            {children}
63        </Context.Provider>
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = <A extends Actions, T extends keyof A, D extends A[T]>(type: T, ...data: (D extends null ? [] : [D])) => void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) => Partial<State> | void : (state: State, data: Actions[K]) => Partial<State> | void;
85};
86import StateContainer from "@company/react-state-container";
87
88const myState = new StateContainer("MyState");
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  "RESET_NUMS",
96  () => ({ nums: [] })
97);
98
99myState.actionHandler(
100  "ADD_NUM",
101  ({ nums }, num: number) => ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  "SET_NUMS",
106  ({}, nums: number[]) => ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115

This is just the first thing that came to my head as far as an API goes. Maybe you can think of something better. While consuming the API makes sense to me, how to write this module is where I'm tripping over myself. For one example: How can I infer the type of the default state like I do in the original code? I can write the defaultState class method using generics but then how do I propagate that generic type into the rest of the class outside of that one method? Is that even possible?

Another question I had pretty quickly. Should I expect the user to provide user-defined types, like for Actions? Should the consumer have to pass in TypeScript stuff at all or is there a way I can just have them pass only the imperative code and then I can layer on some type magic for those who are taking advantage of TypeScript?

The more I start trying to modularize that code the more I feel like I'm just taking the wrong approach altogether or maybe what I want to do is a fool's errand in the first place.

PS - I know I know, Redux. Sadly I don't get to decide.

ANSWER

Answered 2021-Sep-19 at 05:05

PS - I know I know, Redux. Sadly I don't get to decide.

Yes, you're basically re-creating Redux here. More specifically, you're trying to re-create the createSlice functionality of Redux Toolkit. You want to define a mapping of action names to what the action does, and then have the reducer get created automatically. So we can use that prior art to get an idea of how this might work.

Your current brainstorm involves calling functions on the StateContainer object after it has been created. Those functions need to change the types of the StateContainer. This is doable, but it's easier to create the object in one go with all of the information up front.

Let's think about what information needs to be provided and what information needs to be returned.

We need a name, an initialState, and a bunch of actions:

1// Set this accordingly.
2const hookName = "MyState";
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify "null" if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () => ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) => {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) => ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from "react";
41import _cloneDeep from "lodash/cloneDeep";
42
43const Context = React.createContext<
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48>(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) => reducerDispatch({type, data} as Action);
60    return (
61        <Context.Provider value={{ state, dispatch }}>
62            {children}
63        </Context.Provider>
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = <A extends Actions, T extends keyof A, D extends A[T]>(type: T, ...data: (D extends null ? [] : [D])) => void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) => Partial<State> | void : (state: State, data: Actions[K]) => Partial<State> | void;
85};
86import StateContainer from "@company/react-state-container";
87
88const myState = new StateContainer("MyState");
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  "RESET_NUMS",
96  () => ({ nums: [] })
97);
98
99myState.actionHandler(
100  "ADD_NUM",
101  ({ nums }, num: number) => ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  "SET_NUMS",
106  ({}, nums: number[]) => ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115type Config<S, AH> = {
116  name: string;
117  initialState: S;
118  actionHandlers: AH;
119}
120

The type of the state and the type of the actions are generics where S represents State and AH represents ActionHandlers. I'm using letters to make it clear what's a generic and what's an actual type.

We want to put some sort of constraint on the actions. It should be an object whose keys are strings (the action names) and whose values are functions. Those functions take the state and a payload (which will have a different type for each action) and return a new state. Actually your code says that we return Partial<State> | void. I'm not sure what that void accomplishes? But we get this:

1// Set this accordingly.
2const hookName = &quot;MyState&quot;;
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify &quot;null&quot; if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () =&gt; ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) =&gt; {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) =&gt; ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from &quot;react&quot;;
41import _cloneDeep from &quot;lodash/cloneDeep&quot;;
42
43const Context = React.createContext&lt;
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48&gt;(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) =&gt; reducerDispatch({type, data} as Action);
60    return (
61        &lt;Context.Provider value={{ state, dispatch }}&gt;
62            {children}
63        &lt;/Context.Provider&gt;
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = &lt;A extends Actions, T extends keyof A, D extends A[T]&gt;(type: T, ...data: (D extends null ? [] : [D])) =&gt; void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) =&gt; Partial&lt;State&gt; | void : (state: State, data: Actions[K]) =&gt; Partial&lt;State&gt; | void;
85};
86import StateContainer from &quot;@company/react-state-container&quot;;
87
88const myState = new StateContainer(&quot;MyState&quot;);
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  &quot;RESET_NUMS&quot;,
96  () =&gt; ({ nums: [] })
97);
98
99myState.actionHandler(
100  &quot;ADD_NUM&quot;,
101  ({ nums }, num: number) =&gt; ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  &quot;SET_NUMS&quot;,
106  ({}, nums: number[]) =&gt; ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115type Config&lt;S, AH&gt; = {
116  name: string;
117  initialState: S;
118  actionHandlers: AH;
119}
120type GenericActionHandler&lt;S, P&gt; = (state: S, payload: P) =&gt; Partial&lt;S&gt; | void;
121
122type Config&lt;S, AH extends Record&lt;string, GenericActionHandler&lt;S, any&gt;&gt;&gt; = {
123...
124

Our utility is going to take that Config and return a Provider and a hook with properties state and dispatch. It's the dispatch that requires us to do the fancy TypeScript inference in order to get the correct types for the type and data arguments. FYI, having those as two separate arguments does make is slightly harder to ensure that you've got a matching pair.

The typing is similar to the "TypeScript Boilerplate" that you had before. The main difference is that we are working backwards from ActionHandlers to Actions. The nested ternary here handles the situation where there is no second argument.

1// Set this accordingly.
2const hookName = &quot;MyState&quot;;
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify &quot;null&quot; if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () =&gt; ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) =&gt; {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) =&gt; ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from &quot;react&quot;;
41import _cloneDeep from &quot;lodash/cloneDeep&quot;;
42
43const Context = React.createContext&lt;
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48&gt;(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) =&gt; reducerDispatch({type, data} as Action);
60    return (
61        &lt;Context.Provider value={{ state, dispatch }}&gt;
62            {children}
63        &lt;/Context.Provider&gt;
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = &lt;A extends Actions, T extends keyof A, D extends A[T]&gt;(type: T, ...data: (D extends null ? [] : [D])) =&gt; void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) =&gt; Partial&lt;State&gt; | void : (state: State, data: Actions[K]) =&gt; Partial&lt;State&gt; | void;
85};
86import StateContainer from &quot;@company/react-state-container&quot;;
87
88const myState = new StateContainer(&quot;MyState&quot;);
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  &quot;RESET_NUMS&quot;,
96  () =&gt; ({ nums: [] })
97);
98
99myState.actionHandler(
100  &quot;ADD_NUM&quot;,
101  ({ nums }, num: number) =&gt; ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  &quot;SET_NUMS&quot;,
106  ({}, nums: number[]) =&gt; ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115type Config&lt;S, AH&gt; = {
116  name: string;
117  initialState: S;
118  actionHandlers: AH;
119}
120type GenericActionHandler&lt;S, P&gt; = (state: S, payload: P) =&gt; Partial&lt;S&gt; | void;
121
122type Config&lt;S, AH extends Record&lt;string, GenericActionHandler&lt;S, any&gt;&gt;&gt; = {
123...
124type Actions&lt;AH&gt; = {
125  [K in keyof AH]: AH[K] extends GenericActionHandler&lt;any, infer P&gt;
126    ? (unknown extends P ? never : P )
127    : never;
128};
129
130type TypePayloadPair&lt;AH&gt; = {
131  [K in keyof AH]: Actions&lt;AH&gt;[K] extends null | undefined
132    ? [K]
133    : [K, Actions&lt;AH&gt;[K]];
134}[keyof AH];
135
136type Dispatch&lt;AH&gt; = (...args: TypePayloadPair&lt;AH&gt;) =&gt; void;
137

So now, finally, we know the return type of the StateContainer object. Given a state type S and an action handlers object AH, the container type is:

1// Set this accordingly.
2const hookName = &quot;MyState&quot;;
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify &quot;null&quot; if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () =&gt; ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) =&gt; {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) =&gt; ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from &quot;react&quot;;
41import _cloneDeep from &quot;lodash/cloneDeep&quot;;
42
43const Context = React.createContext&lt;
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48&gt;(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) =&gt; reducerDispatch({type, data} as Action);
60    return (
61        &lt;Context.Provider value={{ state, dispatch }}&gt;
62            {children}
63        &lt;/Context.Provider&gt;
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = &lt;A extends Actions, T extends keyof A, D extends A[T]&gt;(type: T, ...data: (D extends null ? [] : [D])) =&gt; void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) =&gt; Partial&lt;State&gt; | void : (state: State, data: Actions[K]) =&gt; Partial&lt;State&gt; | void;
85};
86import StateContainer from &quot;@company/react-state-container&quot;;
87
88const myState = new StateContainer(&quot;MyState&quot;);
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  &quot;RESET_NUMS&quot;,
96  () =&gt; ({ nums: [] })
97);
98
99myState.actionHandler(
100  &quot;ADD_NUM&quot;,
101  ({ nums }, num: number) =&gt; ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  &quot;SET_NUMS&quot;,
106  ({}, nums: number[]) =&gt; ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115type Config&lt;S, AH&gt; = {
116  name: string;
117  initialState: S;
118  actionHandlers: AH;
119}
120type GenericActionHandler&lt;S, P&gt; = (state: S, payload: P) =&gt; Partial&lt;S&gt; | void;
121
122type Config&lt;S, AH extends Record&lt;string, GenericActionHandler&lt;S, any&gt;&gt;&gt; = {
123...
124type Actions&lt;AH&gt; = {
125  [K in keyof AH]: AH[K] extends GenericActionHandler&lt;any, infer P&gt;
126    ? (unknown extends P ? never : P )
127    : never;
128};
129
130type TypePayloadPair&lt;AH&gt; = {
131  [K in keyof AH]: Actions&lt;AH&gt;[K] extends null | undefined
132    ? [K]
133    : [K, Actions&lt;AH&gt;[K]];
134}[keyof AH];
135
136type Dispatch&lt;AH&gt; = (...args: TypePayloadPair&lt;AH&gt;) =&gt; void;
137type StateContainer&lt;S, AH&gt; = {
138  Provider: React.FC&lt;{defaultState?: S}&gt;;
139  useContextState: () =&gt; {
140    state: S;
141    dispatch: Dispatch&lt;AH&gt;;
142  }
143}
144

We'll use that as the return type for the factory function, which I am calling createStateContainer. The argument type is the Config that we wrote earlier.

The conversion of type and data to {type, data} as Action is not really necessary because the React useReducer hook doesn't make any requirements about the action type. You can avoid all as assertions within your function if you pass along the pair of arguments from dispatch as-is.

1// Set this accordingly.
2const hookName = &quot;MyState&quot;;
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify &quot;null&quot; if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () =&gt; ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) =&gt; {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) =&gt; ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from &quot;react&quot;;
41import _cloneDeep from &quot;lodash/cloneDeep&quot;;
42
43const Context = React.createContext&lt;
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48&gt;(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) =&gt; reducerDispatch({type, data} as Action);
60    return (
61        &lt;Context.Provider value={{ state, dispatch }}&gt;
62            {children}
63        &lt;/Context.Provider&gt;
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = &lt;A extends Actions, T extends keyof A, D extends A[T]&gt;(type: T, ...data: (D extends null ? [] : [D])) =&gt; void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) =&gt; Partial&lt;State&gt; | void : (state: State, data: Actions[K]) =&gt; Partial&lt;State&gt; | void;
85};
86import StateContainer from &quot;@company/react-state-container&quot;;
87
88const myState = new StateContainer(&quot;MyState&quot;);
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  &quot;RESET_NUMS&quot;,
96  () =&gt; ({ nums: [] })
97);
98
99myState.actionHandler(
100  &quot;ADD_NUM&quot;,
101  ({ nums }, num: number) =&gt; ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  &quot;SET_NUMS&quot;,
106  ({}, nums: number[]) =&gt; ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115type Config&lt;S, AH&gt; = {
116  name: string;
117  initialState: S;
118  actionHandlers: AH;
119}
120type GenericActionHandler&lt;S, P&gt; = (state: S, payload: P) =&gt; Partial&lt;S&gt; | void;
121
122type Config&lt;S, AH extends Record&lt;string, GenericActionHandler&lt;S, any&gt;&gt;&gt; = {
123...
124type Actions&lt;AH&gt; = {
125  [K in keyof AH]: AH[K] extends GenericActionHandler&lt;any, infer P&gt;
126    ? (unknown extends P ? never : P )
127    : never;
128};
129
130type TypePayloadPair&lt;AH&gt; = {
131  [K in keyof AH]: Actions&lt;AH&gt;[K] extends null | undefined
132    ? [K]
133    : [K, Actions&lt;AH&gt;[K]];
134}[keyof AH];
135
136type Dispatch&lt;AH&gt; = (...args: TypePayloadPair&lt;AH&gt;) =&gt; void;
137type StateContainer&lt;S, AH&gt; = {
138  Provider: React.FC&lt;{defaultState?: S}&gt;;
139  useContextState: () =&gt; {
140    state: S;
141    dispatch: Dispatch&lt;AH&gt;;
142  }
143}
144export default function createStateContainer&lt;
145    S,
146    AH extends Record&lt;string, GenericActionHandler&lt;S, unknown&gt;&gt;
147&gt;({
148    name,
149    initialState,
150    actionHandlers,
151}: Config&lt;S, AH&gt;): StateContainer&lt;S, AH&gt; {
152
153  const Context = React.createContext&lt;
154      {
155          state: S;
156          dispatch: Dispatch&lt;AH&gt;
157      } | undefined
158  &gt;(undefined);
159
160  function reducer(state: S, [type, payload]: TypePayloadPair&lt;AH&gt;): S {
161      const stateClone = _cloneDeep(state);
162      const newState = actionHandlers[type](stateClone, payload);
163      if (!newState) return state;
164      return { ...stateClone, ...newState };
165  }
166
167  function Provider({children, defaultState}: {children?: ReactNode, defaultState?: S}) {
168      const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
169      const dispatch: Dispatch&lt;AH&gt; = (...args) =&gt; reducerDispatch(args);
170      
171      return (
172          &lt;Context.Provider value={{ state, dispatch }}&gt;
173              {children}
174          &lt;/Context.Provider&gt;
175      );
176  }
177
178  function useContextState() {
179      const context = useContext(Context);
180      if (context === undefined) {
181          throw new Error(`use${name} must be used within a ${name}Provider`);
182      }
183      return context;
184  }
185
186  return {
187    Provider,
188    useContextState
189  }
190}
191

Creating an instance has gotten much, much simpler:

1// Set this accordingly.
2const hookName = &quot;MyState&quot;;
3
4// Default state values. Make sure your values have explicit types set.
5const initialState = {
6  nums: [] as Number[]
7};
8
9// Available actions and the data they require. Specify &quot;null&quot; if action requires no data.
10type Actions = {
11  RESET_NUMS: null,
12  ADD_NUM: Number,
13  SET_NUMS: Number[]
14};
15
16// Action handler methods. There must be a handler for each action listed above.
17const actionHandlers: ActionHandlers = {
18  RESET_NUMS: () =&gt; ({ nums: [] }),
19  ADD_NUM: ({ nums }, num) =&gt; {
20    nums.push(num);
21    return { nums };
22  },
23  SET_NUMS: ({}, nums) =&gt; ({ nums })
24};
25
26// Rename these exports accordingly.
27export const MyStateProvider = Provider;
28export const useMyState = useContextState;
29
30
31
32/**************************************************
33 *                                                *
34 * You do not need to modify anything below this. *
35 *                                                *
36 **************************************************/
37
38/* Context Hook Boilerplate */
39
40import React, { useReducer, useContext, ReactNode } from &quot;react&quot;;
41import _cloneDeep from &quot;lodash/cloneDeep&quot;;
42
43const Context = React.createContext&lt;
44    {
45        state: State;
46        dispatch: Dispatch
47    } | undefined
48&gt;(undefined);
49
50function reducer(state: State, action: Action): State {
51    const stateClone = _cloneDeep(state);
52    const newState = actionHandlers[action.type](stateClone, action.data as any);
53    if (!newState) return state;
54    return { ...stateClone, ...newState };
55}
56
57function Provider({children, defaultState}: {children: ReactNode, defaultState?: State}) {
58    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
59    const dispatch: Dispatch = (type, data) =&gt; reducerDispatch({type, data} as Action);
60    return (
61        &lt;Context.Provider value={{ state, dispatch }}&gt;
62            {children}
63        &lt;/Context.Provider&gt;
64    );
65}
66
67function useContextState() {
68    const context = useContext(Context);
69    if (context === undefined) {
70        throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
71    }
72    return context;
73}
74
75/* TypeScript Boilerplate */
76
77type Dispatch = &lt;A extends Actions, T extends keyof A, D extends A[T]&gt;(type: T, ...data: (D extends null ? [] : [D])) =&gt; void
78type State = typeof initialState;
79type ActionsMap = {
80  [K in keyof Actions]: Actions[K] extends null ? { type: K, data?: undefined } : { type: K, data: Actions[K] }
81};
82type Action = ActionsMap[keyof Actions];
83type ActionHandlers = {
84    [K in keyof Actions]: Actions[K] extends null ? (state: State) =&gt; Partial&lt;State&gt; | void : (state: State, data: Actions[K]) =&gt; Partial&lt;State&gt; | void;
85};
86import StateContainer from &quot;@company/react-state-container&quot;;
87
88const myState = new StateContainer(&quot;MyState&quot;);
89
90myState.defaultState({
91  nums: [] as number[]
92});
93
94myState.actionHandler(
95  &quot;RESET_NUMS&quot;,
96  () =&gt; ({ nums: [] })
97);
98
99myState.actionHandler(
100  &quot;ADD_NUM&quot;,
101  ({ nums }, num: number) =&gt; ({ nums: [...nums, num] }) 
102);
103
104myState.actionHandler(
105  &quot;SET_NUMS&quot;,
106  ({}, nums: number[]) =&gt; ({ nums })
107);
108
109const {
110  Provider: MyStateProvider,
111  useStateContainer: useMyState
112} = myState;
113
114export { MyStateProvider, useMyState };
115type Config&lt;S, AH&gt; = {
116  name: string;
117  initialState: S;
118  actionHandlers: AH;
119}
120type GenericActionHandler&lt;S, P&gt; = (state: S, payload: P) =&gt; Partial&lt;S&gt; | void;
121
122type Config&lt;S, AH extends Record&lt;string, GenericActionHandler&lt;S, any&gt;&gt;&gt; = {
123...
124type Actions&lt;AH&gt; = {
125  [K in keyof AH]: AH[K] extends GenericActionHandler&lt;any, infer P&gt;
126    ? (unknown extends P ? never : P )
127    : never;
128};
129
130type TypePayloadPair&lt;AH&gt; = {
131  [K in keyof AH]: Actions&lt;AH&gt;[K] extends null | undefined
132    ? [K]
133    : [K, Actions&lt;AH&gt;[K]];
134}[keyof AH];
135
136type Dispatch&lt;AH&gt; = (...args: TypePayloadPair&lt;AH&gt;) =&gt; void;
137type StateContainer&lt;S, AH&gt; = {
138  Provider: React.FC&lt;{defaultState?: S}&gt;;
139  useContextState: () =&gt; {
140    state: S;
141    dispatch: Dispatch&lt;AH&gt;;
142  }
143}
144export default function createStateContainer&lt;
145    S,
146    AH extends Record&lt;string, GenericActionHandler&lt;S, unknown&gt;&gt;
147&gt;({
148    name,
149    initialState,
150    actionHandlers,
151}: Config&lt;S, AH&gt;): StateContainer&lt;S, AH&gt; {
152
153  const Context = React.createContext&lt;
154      {
155          state: S;
156          dispatch: Dispatch&lt;AH&gt;
157      } | undefined
158  &gt;(undefined);
159
160  function reducer(state: S, [type, payload]: TypePayloadPair&lt;AH&gt;): S {
161      const stateClone = _cloneDeep(state);
162      const newState = actionHandlers[type](stateClone, payload);
163      if (!newState) return state;
164      return { ...stateClone, ...newState };
165  }
166
167  function Provider({children, defaultState}: {children?: ReactNode, defaultState?: S}) {
168      const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
169      const dispatch: Dispatch&lt;AH&gt; = (...args) =&gt; reducerDispatch(args);
170      
171      return (
172          &lt;Context.Provider value={{ state, dispatch }}&gt;
173              {children}
174          &lt;/Context.Provider&gt;
175      );
176  }
177
178  function useContextState() {
179      const context = useContext(Context);
180      if (context === undefined) {
181          throw new Error(`use${name} must be used within a ${name}Provider`);
182      }
183      return context;
184  }
185
186  return {
187    Provider,
188    useContextState
189  }
190}
191import createStateContainer from &quot;./StateContainer&quot;;
192
193export const {
194  Provider: MyStateProvider,
195  useContextState: useMyState
196} = createStateContainer({
197  name: &quot;MyState&quot;,
198  initialState: {
199    nums: [] as number[]
200  },
201  actionHandlers: {
202    RESET_NUMS: () =&gt; ({ nums: [] }),
203    ADD_NUM: ({ nums }, num: number) =&gt; {
204      nums.push(num);
205      return { nums };
206    },
207    SET_NUMS: ({}, nums: number[]) =&gt; ({ nums })
208  }
209});
210

Source https://stackoverflow.com/questions/69231536

QUESTION

How we can call a method from child with State container Blazor Webassembly

Asked 2021-Sep-08 at 16:11

I want to call a method from a child Component and I have this code

AppState:

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22

and Child Component #1:

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22    @inject AppState AppState
23&lt;p&gt;============================&lt;/p&gt;
24&lt;h3&gt;#1 Child&lt;/h3&gt;
25&lt;p&gt;send from Father :&lt;b&gt;@AppState?.MyMsgToChildren&lt;/b&gt;  &lt;/p&gt;
26
27&lt;div class=&quot;col-sm-6&quot;&gt;
28    &lt;button @onclick=&quot;SetMessage&quot;&gt;Send To Parent&lt;/button&gt;
29
30&lt;/div&gt;
31&lt;p&gt;============================&lt;/p&gt;
32
33@code {
34
35    protected override void OnInitialized()
36    {
37        AppState.OnChange += StateHasChanged;
38    }
39    public void Dispose()
40    {
41        AppState.OnChange -= StateHasChanged;
42    }
43    void SetMessage()
44    {
45        AppState.SetMessage(&quot;Message From Child #1&quot;);
46    }
47
48}
49

and the #2 Child is the same code with #1 and I have a parent component :

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22    @inject AppState AppState
23&lt;p&gt;============================&lt;/p&gt;
24&lt;h3&gt;#1 Child&lt;/h3&gt;
25&lt;p&gt;send from Father :&lt;b&gt;@AppState?.MyMsgToChildren&lt;/b&gt;  &lt;/p&gt;
26
27&lt;div class=&quot;col-sm-6&quot;&gt;
28    &lt;button @onclick=&quot;SetMessage&quot;&gt;Send To Parent&lt;/button&gt;
29
30&lt;/div&gt;
31&lt;p&gt;============================&lt;/p&gt;
32
33@code {
34
35    protected override void OnInitialized()
36    {
37        AppState.OnChange += StateHasChanged;
38    }
39    public void Dispose()
40    {
41        AppState.OnChange -= StateHasChanged;
42    }
43    void SetMessage()
44    {
45        AppState.SetMessage(&quot;Message From Child #1&quot;);
46    }
47
48}
49@page &quot;/State&quot;
50
51@inject AppState AppState
52&lt;p&gt;============================&lt;/p&gt;
53&lt;h3&gt;Parent&lt;/h3&gt;
54&lt;p&gt;send from child:&lt;b&gt;@AppState?.MyMessage&lt;/b&gt;  &lt;/p&gt;
55&lt;div class=&quot;col-sm-6&quot;&gt;
56    &lt;button @onclick=&quot;SendMsgTochildren&quot;&gt;Send To Parent&lt;/button&gt;
57&lt;/div&gt;
58&lt;ChildComponent&gt;&lt;/ChildComponent&gt;
59&lt;Child2Component&gt;&lt;/Child2Component&gt;
60
61@code {
62
63    public string MsgToChildren { get; private set; } = &quot;Hi, Im your father - &quot;;
64    int i = 0;
65
66    protected override void OnInitialized()
67    {
68        AppState.OnChange += StateHasChanged;
69    }
70    public void Dispose()
71    {
72        AppState.OnChange -= StateHasChanged;
73    }
74
75    void SendMsgTochildren()
76    {
77        i++;
78        AppState.SetMessageToChildren(MsgToChildren + i.ToString());
79    }
80
81    /* I want to call this method from Child*/
82    void TargetMethod(int page) 
83    { 
84    
85    }
86}
87

this app works well and just I want to call this method: "TargetMethod(int page)" from one of my child components and I need to pass an integer parameter as well

I want to use this code for pagination. I try to make a component(pagination) and add it to each table component and the pagination will be the grandchild of the main component and I know I can use the other ways but I prefer to use state Container to communicate between pagination and others

ANSWER

Answered 2021-Sep-08 at 16:04

I want to call this method: "TargetMethod(int page)" from one of my child components and I need to pass an integer parameter as well

then you can try like this:

AppState.cs

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22    @inject AppState AppState
23&lt;p&gt;============================&lt;/p&gt;
24&lt;h3&gt;#1 Child&lt;/h3&gt;
25&lt;p&gt;send from Father :&lt;b&gt;@AppState?.MyMsgToChildren&lt;/b&gt;  &lt;/p&gt;
26
27&lt;div class=&quot;col-sm-6&quot;&gt;
28    &lt;button @onclick=&quot;SetMessage&quot;&gt;Send To Parent&lt;/button&gt;
29
30&lt;/div&gt;
31&lt;p&gt;============================&lt;/p&gt;
32
33@code {
34
35    protected override void OnInitialized()
36    {
37        AppState.OnChange += StateHasChanged;
38    }
39    public void Dispose()
40    {
41        AppState.OnChange -= StateHasChanged;
42    }
43    void SetMessage()
44    {
45        AppState.SetMessage(&quot;Message From Child #1&quot;);
46    }
47
48}
49@page &quot;/State&quot;
50
51@inject AppState AppState
52&lt;p&gt;============================&lt;/p&gt;
53&lt;h3&gt;Parent&lt;/h3&gt;
54&lt;p&gt;send from child:&lt;b&gt;@AppState?.MyMessage&lt;/b&gt;  &lt;/p&gt;
55&lt;div class=&quot;col-sm-6&quot;&gt;
56    &lt;button @onclick=&quot;SendMsgTochildren&quot;&gt;Send To Parent&lt;/button&gt;
57&lt;/div&gt;
58&lt;ChildComponent&gt;&lt;/ChildComponent&gt;
59&lt;Child2Component&gt;&lt;/Child2Component&gt;
60
61@code {
62
63    public string MsgToChildren { get; private set; } = &quot;Hi, Im your father - &quot;;
64    int i = 0;
65
66    protected override void OnInitialized()
67    {
68        AppState.OnChange += StateHasChanged;
69    }
70    public void Dispose()
71    {
72        AppState.OnChange -= StateHasChanged;
73    }
74
75    void SendMsgTochildren()
76    {
77        i++;
78        AppState.SetMessageToChildren(MsgToChildren + i.ToString());
79    }
80
81    /* I want to call this method from Child*/
82    void TargetMethod(int page) 
83    { 
84    
85    }
86}
87 public class AppState
88    {
89        public Action&lt;int&gt; OnCounterChanged { get; set; }   
90    }
91

Grandparent.razor

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22    @inject AppState AppState
23&lt;p&gt;============================&lt;/p&gt;
24&lt;h3&gt;#1 Child&lt;/h3&gt;
25&lt;p&gt;send from Father :&lt;b&gt;@AppState?.MyMsgToChildren&lt;/b&gt;  &lt;/p&gt;
26
27&lt;div class=&quot;col-sm-6&quot;&gt;
28    &lt;button @onclick=&quot;SetMessage&quot;&gt;Send To Parent&lt;/button&gt;
29
30&lt;/div&gt;
31&lt;p&gt;============================&lt;/p&gt;
32
33@code {
34
35    protected override void OnInitialized()
36    {
37        AppState.OnChange += StateHasChanged;
38    }
39    public void Dispose()
40    {
41        AppState.OnChange -= StateHasChanged;
42    }
43    void SetMessage()
44    {
45        AppState.SetMessage(&quot;Message From Child #1&quot;);
46    }
47
48}
49@page &quot;/State&quot;
50
51@inject AppState AppState
52&lt;p&gt;============================&lt;/p&gt;
53&lt;h3&gt;Parent&lt;/h3&gt;
54&lt;p&gt;send from child:&lt;b&gt;@AppState?.MyMessage&lt;/b&gt;  &lt;/p&gt;
55&lt;div class=&quot;col-sm-6&quot;&gt;
56    &lt;button @onclick=&quot;SendMsgTochildren&quot;&gt;Send To Parent&lt;/button&gt;
57&lt;/div&gt;
58&lt;ChildComponent&gt;&lt;/ChildComponent&gt;
59&lt;Child2Component&gt;&lt;/Child2Component&gt;
60
61@code {
62
63    public string MsgToChildren { get; private set; } = &quot;Hi, Im your father - &quot;;
64    int i = 0;
65
66    protected override void OnInitialized()
67    {
68        AppState.OnChange += StateHasChanged;
69    }
70    public void Dispose()
71    {
72        AppState.OnChange -= StateHasChanged;
73    }
74
75    void SendMsgTochildren()
76    {
77        i++;
78        AppState.SetMessageToChildren(MsgToChildren + i.ToString());
79    }
80
81    /* I want to call this method from Child*/
82    void TargetMethod(int page) 
83    { 
84    
85    }
86}
87 public class AppState
88    {
89        public Action&lt;int&gt; OnCounterChanged { get; set; }   
90    }
91    @inject AppState AppState;
92@page &quot;/grandparent&quot;
93&lt;h1&gt;Counter Value from Grandparent : @Counter&lt;/h1&gt;
94    &lt;Parent/&gt;
95
96
97@code{
98
99    public int Counter { get; set; }
100
101    protected override void OnInitialized()
102    {
103        AppState.OnCounterChanged += OnCounterChanged;
104    }
105
106    private void OnCounterChanged(int counter)
107    {
108        Counter = counter;
109        StateHasChanged();
110    }
111}
112

Parent.razor

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22    @inject AppState AppState
23&lt;p&gt;============================&lt;/p&gt;
24&lt;h3&gt;#1 Child&lt;/h3&gt;
25&lt;p&gt;send from Father :&lt;b&gt;@AppState?.MyMsgToChildren&lt;/b&gt;  &lt;/p&gt;
26
27&lt;div class=&quot;col-sm-6&quot;&gt;
28    &lt;button @onclick=&quot;SetMessage&quot;&gt;Send To Parent&lt;/button&gt;
29
30&lt;/div&gt;
31&lt;p&gt;============================&lt;/p&gt;
32
33@code {
34
35    protected override void OnInitialized()
36    {
37        AppState.OnChange += StateHasChanged;
38    }
39    public void Dispose()
40    {
41        AppState.OnChange -= StateHasChanged;
42    }
43    void SetMessage()
44    {
45        AppState.SetMessage(&quot;Message From Child #1&quot;);
46    }
47
48}
49@page &quot;/State&quot;
50
51@inject AppState AppState
52&lt;p&gt;============================&lt;/p&gt;
53&lt;h3&gt;Parent&lt;/h3&gt;
54&lt;p&gt;send from child:&lt;b&gt;@AppState?.MyMessage&lt;/b&gt;  &lt;/p&gt;
55&lt;div class=&quot;col-sm-6&quot;&gt;
56    &lt;button @onclick=&quot;SendMsgTochildren&quot;&gt;Send To Parent&lt;/button&gt;
57&lt;/div&gt;
58&lt;ChildComponent&gt;&lt;/ChildComponent&gt;
59&lt;Child2Component&gt;&lt;/Child2Component&gt;
60
61@code {
62
63    public string MsgToChildren { get; private set; } = &quot;Hi, Im your father - &quot;;
64    int i = 0;
65
66    protected override void OnInitialized()
67    {
68        AppState.OnChange += StateHasChanged;
69    }
70    public void Dispose()
71    {
72        AppState.OnChange -= StateHasChanged;
73    }
74
75    void SendMsgTochildren()
76    {
77        i++;
78        AppState.SetMessageToChildren(MsgToChildren + i.ToString());
79    }
80
81    /* I want to call this method from Child*/
82    void TargetMethod(int page) 
83    { 
84    
85    }
86}
87 public class AppState
88    {
89        public Action&lt;int&gt; OnCounterChanged { get; set; }   
90    }
91    @inject AppState AppState;
92@page &quot;/grandparent&quot;
93&lt;h1&gt;Counter Value from Grandparent : @Counter&lt;/h1&gt;
94    &lt;Parent/&gt;
95
96
97@code{
98
99    public int Counter { get; set; }
100
101    protected override void OnInitialized()
102    {
103        AppState.OnCounterChanged += OnCounterChanged;
104    }
105
106    private void OnCounterChanged(int counter)
107    {
108        Counter = counter;
109        StateHasChanged();
110    }
111}
112@inject AppState AppState;
113&lt;h1&gt;Counter Value from Parent : @Counter&lt;/h1&gt;
114&lt;Child /&gt;
115
116
117@code{
118
119    public int Counter { get; set; }
120
121    protected override void OnInitialized()
122    {
123        AppState.OnCounterChanged += OnCounterChanged;
124    }
125
126    private void OnCounterChanged(int counter)
127    {
128        Counter = counter;
129        StateHasChanged();
130    }
131}
132&lt;Child /&gt;
133

Child.razor

1public class AppState
2{
3    public string MyMessage { get; private set; }
4    public string MyMsgToChildren { get; private set; }
5
6
7    public event Action OnChange;
8
9    public void SetMessage(string msg)
10    {
11        MyMessage = msg;
12        NotifyStateChanged();
13    }
14
15    public void SetMessageToChildren(string msg)
16    {
17        MyMsgToChildren = msg;
18        NotifyStateChanged();
19    }
20    private void NotifyStateChanged() =&gt; OnChange?.Invoke();
21}
22    @inject AppState AppState
23&lt;p&gt;============================&lt;/p&gt;
24&lt;h3&gt;#1 Child&lt;/h3&gt;
25&lt;p&gt;send from Father :&lt;b&gt;@AppState?.MyMsgToChildren&lt;/b&gt;  &lt;/p&gt;
26
27&lt;div class=&quot;col-sm-6&quot;&gt;
28    &lt;button @onclick=&quot;SetMessage&quot;&gt;Send To Parent&lt;/button&gt;
29
30&lt;/div&gt;
31&lt;p&gt;============================&lt;/p&gt;
32
33@code {
34
35    protected override void OnInitialized()
36    {
37        AppState.OnChange += StateHasChanged;
38    }
39    public void Dispose()
40    {
41        AppState.OnChange -= StateHasChanged;
42    }
43    void SetMessage()
44    {
45        AppState.SetMessage(&quot;Message From Child #1&quot;);
46    }
47
48}
49@page &quot;/State&quot;
50
51@inject AppState AppState
52&lt;p&gt;============================&lt;/p&gt;
53&lt;h3&gt;Parent&lt;/h3&gt;
54&lt;p&gt;send from child:&lt;b&gt;@AppState?.MyMessage&lt;/b&gt;  &lt;/p&gt;
55&lt;div class=&quot;col-sm-6&quot;&gt;
56    &lt;button @onclick=&quot;SendMsgTochildren&quot;&gt;Send To Parent&lt;/button&gt;
57&lt;/div&gt;
58&lt;ChildComponent&gt;&lt;/ChildComponent&gt;
59&lt;Child2Component&gt;&lt;/Child2Component&gt;
60
61@code {
62
63    public string MsgToChildren { get; private set; } = &quot;Hi, Im your father - &quot;;
64    int i = 0;
65
66    protected override void OnInitialized()
67    {
68        AppState.OnChange += StateHasChanged;
69    }
70    public void Dispose()
71    {
72        AppState.OnChange -= StateHasChanged;
73    }
74
75    void SendMsgTochildren()
76    {
77        i++;
78        AppState.SetMessageToChildren(MsgToChildren + i.ToString());
79    }
80
81    /* I want to call this method from Child*/
82    void TargetMethod(int page) 
83    { 
84    
85    }
86}
87 public class AppState
88    {
89        public Action&lt;int&gt; OnCounterChanged { get; set; }   
90    }
91    @inject AppState AppState;
92@page &quot;/grandparent&quot;
93&lt;h1&gt;Counter Value from Grandparent : @Counter&lt;/h1&gt;
94    &lt;Parent/&gt;
95
96
97@code{
98
99    public int Counter { get; set; }
100
101    protected override void OnInitialized()
102    {
103        AppState.OnCounterChanged += OnCounterChanged;
104    }
105
106    private void OnCounterChanged(int counter)
107    {
108        Counter = counter;
109        StateHasChanged();
110    }
111}
112@inject AppState AppState;
113&lt;h1&gt;Counter Value from Parent : @Counter&lt;/h1&gt;
114&lt;Child /&gt;
115
116
117@code{
118
119    public int Counter { get; set; }
120
121    protected override void OnInitialized()
122    {
123        AppState.OnCounterChanged += OnCounterChanged;
124    }
125
126    private void OnCounterChanged(int counter)
127    {
128        Counter = counter;
129        StateHasChanged();
130    }
131}
132&lt;Child /&gt;
133@inject AppState AppState;
134&lt;h1&gt;Counter Value from child : @Counter&lt;/h1&gt;
135&lt;button class=&quot;btn btn-primary&quot; @onclick=&quot;UpdateCounter&quot;&gt; Update Counter&lt;/button&gt;
136@code{
137
138    public int Counter { get; set; }
139
140    private void UpdateCounter()
141    {
142        AppState.OnCounterChanged.Invoke(++Counter);
143    }
144}
145

I'm updating the counter from the child component and invoking the event with an int parameter. (it's just a demo)

Source https://stackoverflow.com/questions/69102127

QUESTION

Bounded value not updated when modified by JsInterop

Asked 2021-Aug-30 at 10:07

I've been trying to implement a custom keyboard solution for one of my projects, so I created a custom component for this. Each time a key is pressed on this component, a javascript function is called and receives the id of the input that has currently the focus.

1function WriteInput(elementId, letter) {
2var myElement = document.getElementById(elementId);
3myElement.value = letter.toLowerCase();
4myElement.dispatchEvent(new Event('change'));
5return true;
6}
7

The jsInterop :

1function WriteInput(elementId, letter) {
2var myElement = document.getElementById(elementId);
3myElement.value = letter.toLowerCase();
4myElement.dispatchEvent(new Event('change'));
5return true;
6}
7    public async Task&lt;bool&gt; WriteInput(string elementId, string letter)
8    {
9        return await _js.InvokeAsync&lt;bool&gt;(&quot;WriteInput&quot;, elementId, letter);
10    } 
11

The KeyBoard Component logic (Keyboard.Id is a value retrieved from a state container) :

1function WriteInput(elementId, letter) {
2var myElement = document.getElementById(elementId);
3myElement.value = letter.toLowerCase();
4myElement.dispatchEvent(new Event('change'));
5return true;
6}
7    public async Task&lt;bool&gt; WriteInput(string elementId, string letter)
8    {
9        return await _js.InvokeAsync&lt;bool&gt;(&quot;WriteInput&quot;, elementId, letter);
10    } 
11private async void OnClick(string letter)
12    {
13        await _FocusService.WriteInput(KeyBoard.Id, letter);
14    }
15

The html :

1function WriteInput(elementId, letter) {
2var myElement = document.getElementById(elementId);
3myElement.value = letter.toLowerCase();
4myElement.dispatchEvent(new Event('change'));
5return true;
6}
7    public async Task&lt;bool&gt; WriteInput(string elementId, string letter)
8    {
9        return await _js.InvokeAsync&lt;bool&gt;(&quot;WriteInput&quot;, elementId, letter);
10    } 
11private async void OnClick(string letter)
12    {
13        await _FocusService.WriteInput(KeyBoard.Id, letter);
14    }
15&lt;input @bind-Value=&quot;ViewModel.Username&quot; OnFocus='()=&gt;KeyBoard.Id=&quot;loginusername&quot;' Id=&quot;loginusername&quot;  /&gt;
16

The issue here is that I can see the letters in the input, but the ViewModel.Username is not updated even if the change event is thrown, what am I missing here, I have the same code as in here and apparently it is working : Changing an Input value in Blazor by javascript doesn't change it's binded property value

Thanks for your help

ANSWER

Answered 2021-Aug-30 at 10:07

I've taken the code you provided down to a bare bones working model that shows the bind value being updated. The JS is in site.js which is referenced in _Host.cshtml.

1function WriteInput(elementId, letter) {
2var myElement = document.getElementById(elementId);
3myElement.value = letter.toLowerCase();
4myElement.dispatchEvent(new Event('change'));
5return true;
6}
7    public async Task&lt;bool&gt; WriteInput(string elementId, string letter)
8    {
9        return await _js.InvokeAsync&lt;bool&gt;(&quot;WriteInput&quot;, elementId, letter);
10    } 
11private async void OnClick(string letter)
12    {
13        await _FocusService.WriteInput(KeyBoard.Id, letter);
14    }
15&lt;input @bind-Value=&quot;ViewModel.Username&quot; OnFocus='()=&gt;KeyBoard.Id=&quot;loginusername&quot;' Id=&quot;loginusername&quot;  /&gt;
16@page &quot;/&quot;
17&lt;h3&gt;SoftKeyboard&lt;/h3&gt;
18&lt;div class=&quot;m-1 p-2&quot;&gt;
19    First Name: &lt;input @bind-value=&quot;_model.FirstName&quot; id=&quot;FirstNameField&quot; /&gt;
20&lt;/div&gt;
21&lt;div class=&quot;m-1 p-2&quot;&gt;
22    Surname: &lt;input @bind-value=&quot;_model.Surname&quot; id=&quot;SurnameField&quot; /&gt;
23&lt;/div&gt;
24&lt;div class=&quot;m-1 p-2&quot;&gt;
25    UserName: &lt;input @bind-value=&quot;_model.UserName&quot; id=&quot;UserNameField&quot; /&gt;
26&lt;/div&gt;
27
28&lt;div class=&quot;m-1 p-2&quot;&gt;
29   Focus Control:  &lt;select @onchange=&quot;FieldSelector&quot;&gt;
30        &lt;option value=&quot;FirstNameField&quot;&gt;First Name&lt;/option&gt;
31        &lt;option value=&quot;SurnameField&quot;&gt;Surname&lt;/option&gt;
32        &lt;option value=&quot;UserNameField&quot;&gt;User Name&lt;/option&gt;
33    &lt;/select&gt;
34&lt;/div&gt;
35
36&lt;div class=&quot;m-1 p-2&quot;&gt;
37    &lt;button class=&quot;btn btn-dark ms-1&quot; @onclick=&quot;((e) =&gt; SoftClick('A'))&quot;&gt;A&lt;/button&gt;
38    &lt;button class=&quot;btn btn-dark ms-1&quot; @onclick=&quot;((e) =&gt; SoftClick('S'))&quot;&gt;S&lt;/button&gt;
39    &lt;button class=&quot;btn btn-dark ms-1&quot; @onclick=&quot;((e) =&gt; SoftClick('D'))&quot;&gt;D&lt;/button&gt;
40    &lt;button class=&quot;btn btn-dark ms-1&quot; @onclick=&quot;((e) =&gt; SoftClick('F'))&quot;&gt;F&lt;/button&gt;
41    &lt;button class=&quot;btn btn-dark ms-1&quot; @onclick=&quot;((e) =&gt; SoftClick('K'))&quot;&gt;K&lt;/button&gt;
42&lt;/div&gt;
43
44&lt;div class=&quot;m-1 p-2&quot;&gt;
45    &lt;div&gt;First Name: @_model.FirstName&lt;/div&gt;
46    &lt;div&gt;Surname: @_model.Surname&lt;/div&gt;
47    &lt;div&gt;User Name: @_model.UserName&lt;/div&gt;
48&lt;/div&gt;
49
50@code {
51
52    [Inject] IJSRuntime _js { get; set; }
53
54    async void SoftClick(char key)
55    {
56        await _js.InvokeAsync&lt;bool&gt;(&quot;WriteInput&quot;, selectedField, key);
57    }
58
59    void FieldSelector(ChangeEventArgs e)
60    {
61        selectedField = e.Value.ToString();
62    }
63
64    private string selectedField = &quot;FirstNameField&quot;;
65
66    public class Model
67    {
68        public string UserName { get; set; }
69        public string FirstName { get; set; }
70        public string Surname { get; set; }
71    }
72
73    Model _model = new Model();
74}
75

Source https://stackoverflow.com/questions/68949761

QUESTION

Blazor components: How to communicate from grandchild to child to parent or grandchild to parent

Asked 2021-Jul-11 at 17:21

In my Blazor server app, I have a page called Home that is basically a search page. On the Home page (the "parent") there is a form with controls to filter the data stored in a backend database. Each row of the search results has an Edit button that displays a bootstrap dialog allowing one to edit the data. After the Save button on the UpdateDocumentNumber (grandchild) component is clicked I want to refresh the search results in the Home page (parent).

  • Parent: Home (page) with search results grid; embeds a DisplayDocumentNumber component for each item
  • Child: DisplayDocumentNumber component, which has its own (child) UpdateDocumentNumber component
  • Grandchild: UpdateDocumentNumber component - bootstrap dialog

My understanding is I can use Event Callback to do this; however, while I can raise/invoke the Event Callback from grandchild to child, I cannot seem to then inform the parent from the child.

Some more details...

As the Home ("parent" page) iterates over the returned list of items it inserts a <DisplayDocumentNumber> component (the "child"), and then the <DisplayDocumentNumber> component has a component to reference the Edit dialog. Here's the Home page iterating over the search results:

1&lt;tbody&gt;
2    @foreach (var documentNumber in DocumentNumbers)
3        {
4            &lt;DisplayDocumentNumber DocumentNumber=&quot;documentNumber&quot; /&gt; // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5        }
6&lt;/tbody&gt;
7

Here's the DisplayDocumentNumber component:

1&lt;tbody&gt;
2    @foreach (var documentNumber in DocumentNumbers)
3        {
4            &lt;DisplayDocumentNumber DocumentNumber=&quot;documentNumber&quot; /&gt; // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5        }
6&lt;/tbody&gt;
7public partial class DisplayDocumentNumber : ComponentBase
8{
9    [Parameter]
10    public DocumentNumberDto DocumentNumber { get; set; }
11    [Parameter]
12    public EventCallback&lt;bool&gt; OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14

Note the public EventCallback<bool> OnDocumentNumberUpdatedEventCallback { get; set; }. This works properly from grandchild to child.

Inside the DisplayDocumentNumber.razor component is the row that gets rendered for each document number in the search results, including an Edit button that has a DOM event to show the bootstrap dialog. And, finally, as mentioned above, there is the <UpdateDocumentNumberDialog> component I.e.

1&lt;tbody&gt;
2    @foreach (var documentNumber in DocumentNumbers)
3        {
4            &lt;DisplayDocumentNumber DocumentNumber=&quot;documentNumber&quot; /&gt; // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5        }
6&lt;/tbody&gt;
7public partial class DisplayDocumentNumber : ComponentBase
8{
9    [Parameter]
10    public DocumentNumberDto DocumentNumber { get; set; }
11    [Parameter]
12    public EventCallback&lt;bool&gt; OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14&lt;tr&gt;
15    &lt;td&gt;@DocumentNumber.Column1Name&lt;/td&gt;
16    &lt;td&gt;@DocumentNumber.Column2Name&lt;/td&gt;
17    ...etc
18    &lt;td&gt;&lt;button class=&quot;btn btn-primary table-btn&quot; @onclick=&quot;@(() =&gt; ShowEditDocumentNumberDialog(DocumentNumber))&quot;&gt;Edit&lt;/button&gt;&lt;/td&gt;
19    &lt;UpdateDocumentNumberDialog @ref=&quot;UpdateDocNumDialog&quot; DocumentNumberForUpdating=&quot;DocumentNumber&quot; DocumentNumberUpdatedEventCallback=&quot;@UpdateDocumentNumberDialog_OnDialogClose&quot;&gt;&lt;/UpdateDocumentNumberDialog&gt;
20&lt;/tr&gt;
21

If Event Callbacks only work from a grandchild to child or child to parent, do I have to create some kind of state container as described by Chris Sainty (https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/)? Or, am I missing something about Event Callbacks? I've tried to eliminate the child component, but failed because the Edit button always showed the last item iterated in the grid.

ANSWER

Answered 2021-Jul-10 at 22:14

I can think of a few options. One is to give the grandchild access to the main page. This does not require an event at all:

Parent.razor

1&lt;tbody&gt;
2    @foreach (var documentNumber in DocumentNumbers)
3        {
4            &lt;DisplayDocumentNumber DocumentNumber=&quot;documentNumber&quot; /&gt; // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5        }
6&lt;/tbody&gt;
7public partial class DisplayDocumentNumber : ComponentBase
8{
9    [Parameter]
10    public DocumentNumberDto DocumentNumber { get; set; }
11    [Parameter]
12    public EventCallback&lt;bool&gt; OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14&lt;tr&gt;
15    &lt;td&gt;@DocumentNumber.Column1Name&lt;/td&gt;
16    &lt;td&gt;@DocumentNumber.Column2Name&lt;/td&gt;
17    ...etc
18    &lt;td&gt;&lt;button class=&quot;btn btn-primary table-btn&quot; @onclick=&quot;@(() =&gt; ShowEditDocumentNumberDialog(DocumentNumber))&quot;&gt;Edit&lt;/button&gt;&lt;/td&gt;
19    &lt;UpdateDocumentNumberDialog @ref=&quot;UpdateDocNumDialog&quot; DocumentNumberForUpdating=&quot;DocumentNumber&quot; DocumentNumberUpdatedEventCallback=&quot;@UpdateDocumentNumberDialog_OnDialogClose&quot;&gt;&lt;/UpdateDocumentNumberDialog&gt;
20&lt;/tr&gt;
21&lt;CascadingValue Value=&quot;this&quot;&gt;
22    Body of page, somewhere in there including &lt;Grandchild/&gt;
23&lt;/CascadingValue&gt;
24
25@code{
26    async Task DoSomething(){
27    }
28}
29

Grandchild.razor

1&lt;tbody&gt;
2    @foreach (var documentNumber in DocumentNumbers)
3        {
4            &lt;DisplayDocumentNumber DocumentNumber=&quot;documentNumber&quot; /&gt; // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5        }
6&lt;/tbody&gt;
7public partial class DisplayDocumentNumber : ComponentBase
8{
9    [Parameter]
10    public DocumentNumberDto DocumentNumber { get; set; }
11    [Parameter]
12    public EventCallback&lt;bool&gt; OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14&lt;tr&gt;
15    &lt;td&gt;@DocumentNumber.Column1Name&lt;/td&gt;
16    &lt;td&gt;@DocumentNumber.Column2Name&lt;/td&gt;
17    ...etc
18    &lt;td&gt;&lt;button class=&quot;btn btn-primary table-btn&quot; @onclick=&quot;@(() =&gt; ShowEditDocumentNumberDialog(DocumentNumber))&quot;&gt;Edit&lt;/button&gt;&lt;/td&gt;
19    &lt;UpdateDocumentNumberDialog @ref=&quot;UpdateDocNumDialog&quot; DocumentNumberForUpdating=&quot;DocumentNumber&quot; DocumentNumberUpdatedEventCallback=&quot;@UpdateDocumentNumberDialog_OnDialogClose&quot;&gt;&lt;/UpdateDocumentNumberDialog&gt;
20&lt;/tr&gt;
21&lt;CascadingValue Value=&quot;this&quot;&gt;
22    Body of page, somewhere in there including &lt;Grandchild/&gt;
23&lt;/CascadingValue&gt;
24
25@code{
26    async Task DoSomething(){
27    }
28}
29@code {
30    [CascadingParameter]
31    public Parent MainPage {get; set;} // Or whatever your main page is called
32
33    async Task StartSomething (){
34        if (MainPage is not null) MainPage.DoSomething();
35    }
36}
37

Source https://stackoverflow.com/questions/68308779

QUESTION

Is there a way to trigger two consecutive events in Blazor with a single click?

Asked 2021-Mar-16 at 20:47

Solved - Solution by @Henk Holterman

By making the method async and adding a Task.Delay into the method, you can execute both by calling the second method from the first after the delay.

1private async Task ProcessSelection(SetCardUiModel setCard)
2    {
3        numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
4
5        if (numberOfSelected == 3)
6        {
7            var setSubmission = uniqueCardCombinations.Where(card =&gt; card.BackGroundColor == &quot;yellow&quot;).ToList();
8
9            var potentialSet = _mapper.Map&lt;List&lt;SetCardUiModel&gt;, List&lt;SetCard&gt;&gt;(setSubmission);
10            var isSet = _cardHelperService.VerifySet(potentialSet);
11
12            _uiHelperService.ChangeSetBackgroundColorOnSubmissionOutcome(setSubmission, isSet);
13
14            await Task.Delay(1000);
15            ProcessSetReplacement();
16        };
17    }
18

Background
I have created the card game called SET in Blazor WASM. In this game you click 3 cards and this can result in either a succesful SET, or a false submission. It works well and now I want to add an additional functionality to signal the outcome of the submission.

Desired result
First method: Upon clicking the 3rd card, the 3 cards should get a green (correct) or red (false) background and after x amount of time (say 1 second) a second method should fire.
Second method: If set was correct, replace the 3 cards with 3 new ones. If set was false, reset background color to white and reset border color to black.

Actual current result
What currently happens is that the result of the first method (green/red background) doesn't show, because there is only one callbackevent. So it does get executed, but it won't become visible until the second method is also executed, by which time either the cards have been replaced, or the backgroundcolor/bordercolor have been reset.

Tried so far
I tried to separate the two methods within the @onclick event, but there is still only one eventcallback and I could not find another way to do this so far, nor on stack overflow.

1private async Task ProcessSelection(SetCardUiModel setCard)
2    {
3        numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
4
5        if (numberOfSelected == 3)
6        {
7            var setSubmission = uniqueCardCombinations.Where(card =&gt; card.BackGroundColor == &quot;yellow&quot;).ToList();
8
9            var potentialSet = _mapper.Map&lt;List&lt;SetCardUiModel&gt;, List&lt;SetCard&gt;&gt;(setSubmission);
10            var isSet = _cardHelperService.VerifySet(potentialSet);
11
12            _uiHelperService.ChangeSetBackgroundColorOnSubmissionOutcome(setSubmission, isSet);
13
14            await Task.Delay(1000);
15            ProcessSetReplacement();
16        };
17    }
18@onclick=&quot;() =&gt; { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }
19

The most notable difference is that EventCallback is a single-cast event handler, whereas .NET events are multi-cast. Blazor EventCallback is meant to be assigned a single value and can only call back a single method. https://blazor-university.com/components/component-events/

I also tried the "State Container" as described here by Chris Sainty, but that only re-renders it and I couldn't adjust it to my situation (not sure that's even possible tbh): https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/

Code
I do intend to clean up the code once I get it working, making it more descriptive and splitting up the ProcessSetReplacement a bit more, but wanted to make it work first.

If you need more background/code info either let me know or you can find the entire repository here: https://github.com/John-Experimental/GamesInBlazor/tree/21_AddSetValidationVisualisation

SetPage.razor:

1private async Task ProcessSelection(SetCardUiModel setCard)
2    {
3        numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
4
5        if (numberOfSelected == 3)
6        {
7            var setSubmission = uniqueCardCombinations.Where(card =&gt; card.BackGroundColor == &quot;yellow&quot;).ToList();
8
9            var potentialSet = _mapper.Map&lt;List&lt;SetCardUiModel&gt;, List&lt;SetCard&gt;&gt;(setSubmission);
10            var isSet = _cardHelperService.VerifySet(potentialSet);
11
12            _uiHelperService.ChangeSetBackgroundColorOnSubmissionOutcome(setSubmission, isSet);
13
14            await Task.Delay(1000);
15            ProcessSetReplacement();
16        };
17    }
18@onclick=&quot;() =&gt; { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }
19&lt;div class=&quot;cardsContainer&quot;&gt;
20    @for (int i = 0; i &lt; numberOfCardsVisible; i++)
21    {
22        var index = i;
23        &lt;div class=&quot;card @lineClass&quot; style=&quot;background-color:@uniqueCardCombinations[index].BackGroundColor; 
24                border-color:@uniqueCardCombinations[index].BorderColor;&quot; 
25                @onclick=&quot;() =&gt; { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }&quot;&gt;
26            @for (int j = 0; j &lt; uniqueCardCombinations[i].Count; j++)
27            {
28                &lt;div class=&quot;@uniqueCardCombinations[i].Shape @uniqueCardCombinations[i].Color @uniqueCardCombinations[i].Border&quot;&gt;&lt;/div&gt;
29            }
30        &lt;/div&gt;
31    }
32&lt;/div&gt;
33

Relevant parts of SetPage.razor.cs (code behind)

1private async Task ProcessSelection(SetCardUiModel setCard)
2    {
3        numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
4
5        if (numberOfSelected == 3)
6        {
7            var setSubmission = uniqueCardCombinations.Where(card =&gt; card.BackGroundColor == &quot;yellow&quot;).ToList();
8
9            var potentialSet = _mapper.Map&lt;List&lt;SetCardUiModel&gt;, List&lt;SetCard&gt;&gt;(setSubmission);
10            var isSet = _cardHelperService.VerifySet(potentialSet);
11
12            _uiHelperService.ChangeSetBackgroundColorOnSubmissionOutcome(setSubmission, isSet);
13
14            await Task.Delay(1000);
15            ProcessSetReplacement();
16        };
17    }
18@onclick=&quot;() =&gt; { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }
19&lt;div class=&quot;cardsContainer&quot;&gt;
20    @for (int i = 0; i &lt; numberOfCardsVisible; i++)
21    {
22        var index = i;
23        &lt;div class=&quot;card @lineClass&quot; style=&quot;background-color:@uniqueCardCombinations[index].BackGroundColor; 
24                border-color:@uniqueCardCombinations[index].BorderColor;&quot; 
25                @onclick=&quot;() =&gt; { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }&quot;&gt;
26            @for (int j = 0; j &lt; uniqueCardCombinations[i].Count; j++)
27            {
28                &lt;div class=&quot;@uniqueCardCombinations[i].Shape @uniqueCardCombinations[i].Color @uniqueCardCombinations[i].Border&quot;&gt;&lt;/div&gt;
29            }
30        &lt;/div&gt;
31    }
32&lt;/div&gt;
33private void ProcessSelection(SetCardUiModel setCard)
34    {
35        numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
36
37        if (numberOfSelected == 3)
38        {
39            var setSubmission = uniqueCardCombinations.Where(card =&gt; card.BackGroundColor == &quot;yellow&quot;).ToList();
40            var potentialSet = _mapper.Map&lt;List&lt;SetCardUiModel&gt;, List&lt;SetCard&gt;&gt;(setSubmission);
41            var isSet = _cardHelperService.VerifySet(potentialSet);
42
43            _uiHelperService.SignalSetSubmissionOutcome(setSubmission, isSet);
44        };
45    }
46
47private void ProcessSetReplacement()
48    {
49        // If it wasn't a set submission, you do nothing
50        if (numberOfSelected == 3)
51        {
52            var redBorderedCards = uniqueCardCombinations.Where(card =&gt; card.BorderColor == &quot;red&quot;).ToList();
53            var countGreenBorders = uniqueCardCombinations.Count(card =&gt; card.BorderColor == &quot;green&quot;);
54
55            // The while ensures that the 'ProcessSelection' function, which is also called, has run first
56            while (redBorderedCards.Count == 0 &amp;&amp; countGreenBorders == 0)
57            {
58                Thread.Sleep(125);
59                redBorderedCards = uniqueCardCombinations.Where(card =&gt; card.BorderColor == &quot;red&quot;).ToList();
60                countGreenBorders = uniqueCardCombinations.Count(card =&gt; card.BorderColor == &quot;green&quot;);
61            }
62
63            // Wait 1.5 seconds so that the user can see the set outcome from 'ProcessSelection' before removing it
64            Thread.Sleep(1500);
65
66            if (countGreenBorders == 3)
67            {
68                // Replace the set by removing the set submission entirely from the list
69                uniqueCardCombinations.RemoveAll(card =&gt; card.BackGroundColor == &quot;yellow&quot;);
70                numberOfSelected = 0;
71
72                // Check if the field currently shows more cards than normal (can happen if there was no set previously)
73                // If there are more cards, then remove 3 cards again to bring it back down to 'normal'
74                numberOfCardsVisible -= numberOfCardsVisible &gt; settings.numberOfCardsVisible ? 3 : 0;
75
76                EnsureSetExistsOnField();
77            }
78            else
79            {
80                foreach (var card in redBorderedCards)
81                {
82                    card.BackGroundColor = &quot;white&quot;;
83                    card.BorderColor = &quot;black&quot;;
84                }
85            }
86        };
87    }
88

ANSWER

Answered 2021-Mar-15 at 21:02

Do not call both methods in onclick.

  • Call ProcessSelection, process result and set red/green there.
  • Call StateHasChanged()
  • Set timer to one second in ProcessSelection. Use Timer from System.Timers.
  • On Elapsed of the timer call ProcessSetReplacement.

Source https://stackoverflow.com/questions/66645358

QUESTION

Flatlist undefined is not an object React-native

Asked 2021-Jan-13 at 17:23

I am building a simple React-native app with Expo for rating Github repositories and ran into a nasty issue. When I am trying to render a list of the repositories with Flatlist it throws me the following error: undefined is not an object (evaluating 'repository.fullName'); although my code is pretty much identical to the one in React-native docs. Here is the RepositoryList.jsx where the Flatlist is being rendered:

1
2import React from 'react';
3import { FlatList, View, StyleSheet } from 'react-native';
4import RepositoryItem from './RepositoryItem'
5
6const styles = StyleSheet.create({
7  separator: {
8    height: 10,
9  },
10});
11
12const repositories = [
13  {
14    id: 'rails.rails',
15    fullName: 'rails/rails',
16    description: 'Ruby on Rails',
17    language: 'Ruby',
18    forksCount: 18349,
19    stargazersCount: 45377,
20    ratingAverage: 100,
21    reviewCount: 2,
22    ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4',
23  },
24  {
25    id: 'reduxjs.redux',
26    fullName: 'reduxjs/redux',
27    description: 'Predictable state container for JavaScript apps',
28    language: 'TypeScript',
29    forksCount: 13902,
30    stargazersCount: 52869,
31    ratingAverage: 0,
32    reviewCount: 0,
33    ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4',
34  }
35];
36
37const ItemSeparator = () =&gt; &lt;View style={styles.separator} /&gt;;
38
39const RepositoryList = () =&gt; {
40  return (
41    &lt;FlatList
42      data={repositories}
43      ItemSeparatorComponent={ItemSeparator}
44      renderItem={({repository}) =&gt; &lt;RepositoryItem repository={repository}/&gt; }
45    /&gt;
46  );
47};
48
49export default RepositoryList
50

and RepositoryItem.jsx which should be rendered within the Flatlist:

1
2import React from 'react';
3import { FlatList, View, StyleSheet } from 'react-native';
4import RepositoryItem from './RepositoryItem'
5
6const styles = StyleSheet.create({
7  separator: {
8    height: 10,
9  },
10});
11
12const repositories = [
13  {
14    id: 'rails.rails',
15    fullName: 'rails/rails',
16    description: 'Ruby on Rails',
17    language: 'Ruby',
18    forksCount: 18349,
19    stargazersCount: 45377,
20    ratingAverage: 100,
21    reviewCount: 2,
22    ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4',
23  },
24  {
25    id: 'reduxjs.redux',
26    fullName: 'reduxjs/redux',
27    description: 'Predictable state container for JavaScript apps',
28    language: 'TypeScript',
29    forksCount: 13902,
30    stargazersCount: 52869,
31    ratingAverage: 0,
32    reviewCount: 0,
33    ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4',
34  }
35];
36
37const ItemSeparator = () =&gt; &lt;View style={styles.separator} /&gt;;
38
39const RepositoryList = () =&gt; {
40  return (
41    &lt;FlatList
42      data={repositories}
43      ItemSeparatorComponent={ItemSeparator}
44      renderItem={({repository}) =&gt; &lt;RepositoryItem repository={repository}/&gt; }
45    /&gt;
46  );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) =&gt; {
54  return(
55    &lt;View style={styles.item}&gt;
56      &lt;Text&gt;Full name:{repository.fullName}&lt;/Text&gt;
57      &lt;Text&gt;Description:{repository.description}&lt;/Text&gt;
58      &lt;Text&gt;Language:{repository.language}&lt;/Text&gt;
59      &lt;Text&gt;Stars:{repository.stargazersCount}&lt;/Text&gt;
60      &lt;Text&gt;Forks:{repository.forksCount}&lt;/Text&gt;
61      &lt;Text&gt;Reviews:{repository.reviewCount}&lt;/Text&gt;
62      &lt;Text&gt;Rating:{repository.ratingAverage}&lt;/Text&gt;
63    &lt;/View&gt;
64  )
65}
66
67styles = StyleSheet.create({
68  item: {
69    marginHorizontal: 16,
70    backgroundColor: 'darkorange'
71  },
72});
73
74export default RepositoryItem
75

After doing my research I found that a lot of people have run into this issue too, and apparently it persists since 0.59 (my React-native is on 0.62, Windows). Apparently the error is being cause by a babel module '@babel/plugin-proposal-class-properties' and the solution would be deleting this module from .babelrc, according to this Github thread https://github.com/facebook/react-native/issues/24421. The problem is that my babel.config.js is extremely simple, and I don't see how I can exclude this module from being required for babel to work. My babel.config.js:

1
2import React from 'react';
3import { FlatList, View, StyleSheet } from 'react-native';
4import RepositoryItem from './RepositoryItem'
5
6const styles = StyleSheet.create({
7  separator: {
8    height: 10,
9  },
10});
11
12const repositories = [
13  {
14    id: 'rails.rails',
15    fullName: 'rails/rails',
16    description: 'Ruby on Rails',
17    language: 'Ruby',
18    forksCount: 18349,
19    stargazersCount: 45377,
20    ratingAverage: 100,
21    reviewCount: 2,
22    ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4',
23  },
24  {
25    id: 'reduxjs.redux',
26    fullName: 'reduxjs/redux',
27    description: 'Predictable state container for JavaScript apps',
28    language: 'TypeScript',
29    forksCount: 13902,
30    stargazersCount: 52869,
31    ratingAverage: 0,
32    reviewCount: 0,
33    ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4',
34  }
35];
36
37const ItemSeparator = () =&gt; &lt;View style={styles.separator} /&gt;;
38
39const RepositoryList = () =&gt; {
40  return (
41    &lt;FlatList
42      data={repositories}
43      ItemSeparatorComponent={ItemSeparator}
44      renderItem={({repository}) =&gt; &lt;RepositoryItem repository={repository}/&gt; }
45    /&gt;
46  );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) =&gt; {
54  return(
55    &lt;View style={styles.item}&gt;
56      &lt;Text&gt;Full name:{repository.fullName}&lt;/Text&gt;
57      &lt;Text&gt;Description:{repository.description}&lt;/Text&gt;
58      &lt;Text&gt;Language:{repository.language}&lt;/Text&gt;
59      &lt;Text&gt;Stars:{repository.stargazersCount}&lt;/Text&gt;
60      &lt;Text&gt;Forks:{repository.forksCount}&lt;/Text&gt;
61      &lt;Text&gt;Reviews:{repository.reviewCount}&lt;/Text&gt;
62      &lt;Text&gt;Rating:{repository.ratingAverage}&lt;/Text&gt;
63    &lt;/View&gt;
64  )
65}
66
67styles = StyleSheet.create({
68  item: {
69    marginHorizontal: 16,
70    backgroundColor: 'darkorange'
71  },
72});
73
74export default RepositoryItem
75module.exports = function(api) {
76  api.cache(true);
77  return {
78    presets: ['babel-preset-expo'],
79  };
80};
81

Perhaps there is a way to exclude it through tweaking babel in node_modules, but this solution seems unlikely. Any help or suggestions regarding this issue will be greatly appreciated!

ANSWER

Answered 2021-Jan-13 at 17:23

I think your problem consists in destructuring repository in your renderItem method of the FlatList. You cannot just destructure whatever you want, you have to destructure item from the Flatlist.

Try this way:

1
2import React from 'react';
3import { FlatList, View, StyleSheet } from 'react-native';
4import RepositoryItem from './RepositoryItem'
5
6const styles = StyleSheet.create({
7  separator: {
8    height: 10,
9  },
10});
11
12const repositories = [
13  {
14    id: 'rails.rails',
15    fullName: 'rails/rails',
16    description: 'Ruby on Rails',
17    language: 'Ruby',
18    forksCount: 18349,
19    stargazersCount: 45377,
20    ratingAverage: 100,
21    reviewCount: 2,
22    ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4',
23  },
24  {
25    id: 'reduxjs.redux',
26    fullName: 'reduxjs/redux',
27    description: 'Predictable state container for JavaScript apps',
28    language: 'TypeScript',
29    forksCount: 13902,
30    stargazersCount: 52869,
31    ratingAverage: 0,
32    reviewCount: 0,
33    ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4',
34  }
35];
36
37const ItemSeparator = () =&gt; &lt;View style={styles.separator} /&gt;;
38
39const RepositoryList = () =&gt; {
40  return (
41    &lt;FlatList
42      data={repositories}
43      ItemSeparatorComponent={ItemSeparator}
44      renderItem={({repository}) =&gt; &lt;RepositoryItem repository={repository}/&gt; }
45    /&gt;
46  );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) =&gt; {
54  return(
55    &lt;View style={styles.item}&gt;
56      &lt;Text&gt;Full name:{repository.fullName}&lt;/Text&gt;
57      &lt;Text&gt;Description:{repository.description}&lt;/Text&gt;
58      &lt;Text&gt;Language:{repository.language}&lt;/Text&gt;
59      &lt;Text&gt;Stars:{repository.stargazersCount}&lt;/Text&gt;
60      &lt;Text&gt;Forks:{repository.forksCount}&lt;/Text&gt;
61      &lt;Text&gt;Reviews:{repository.reviewCount}&lt;/Text&gt;
62      &lt;Text&gt;Rating:{repository.ratingAverage}&lt;/Text&gt;
63    &lt;/View&gt;
64  )
65}
66
67styles = StyleSheet.create({
68  item: {
69    marginHorizontal: 16,
70    backgroundColor: 'darkorange'
71  },
72});
73
74export default RepositoryItem
75module.exports = function(api) {
76  api.cache(true);
77  return {
78    presets: ['babel-preset-expo'],
79  };
80};
81const RepositoryList = () =&gt; {
82  return (
83    &lt;FlatList
84      data={repositories}
85      ItemSeparatorComponent={ItemSeparator}
86      renderItem={({ item }) =&gt; &lt;RepositoryItem repository={item}/&gt; }
87    /&gt;
88  );
89};
90

Or, if you really want to

1
2import React from 'react';
3import { FlatList, View, StyleSheet } from 'react-native';
4import RepositoryItem from './RepositoryItem'
5
6const styles = StyleSheet.create({
7  separator: {
8    height: 10,
9  },
10});
11
12const repositories = [
13  {
14    id: 'rails.rails',
15    fullName: 'rails/rails',
16    description: 'Ruby on Rails',
17    language: 'Ruby',
18    forksCount: 18349,
19    stargazersCount: 45377,
20    ratingAverage: 100,
21    reviewCount: 2,
22    ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4',
23  },
24  {
25    id: 'reduxjs.redux',
26    fullName: 'reduxjs/redux',
27    description: 'Predictable state container for JavaScript apps',
28    language: 'TypeScript',
29    forksCount: 13902,
30    stargazersCount: 52869,
31    ratingAverage: 0,
32    reviewCount: 0,
33    ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4',
34  }
35];
36
37const ItemSeparator = () =&gt; &lt;View style={styles.separator} /&gt;;
38
39const RepositoryList = () =&gt; {
40  return (
41    &lt;FlatList
42      data={repositories}
43      ItemSeparatorComponent={ItemSeparator}
44      renderItem={({repository}) =&gt; &lt;RepositoryItem repository={repository}/&gt; }
45    /&gt;
46  );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) =&gt; {
54  return(
55    &lt;View style={styles.item}&gt;
56      &lt;Text&gt;Full name:{repository.fullName}&lt;/Text&gt;
57      &lt;Text&gt;Description:{repository.description}&lt;/Text&gt;
58      &lt;Text&gt;Language:{repository.language}&lt;/Text&gt;
59      &lt;Text&gt;Stars:{repository.stargazersCount}&lt;/Text&gt;
60      &lt;Text&gt;Forks:{repository.forksCount}&lt;/Text&gt;
61      &lt;Text&gt;Reviews:{repository.reviewCount}&lt;/Text&gt;
62      &lt;Text&gt;Rating:{repository.ratingAverage}&lt;/Text&gt;
63    &lt;/View&gt;
64  )
65}
66
67styles = StyleSheet.create({
68  item: {
69    marginHorizontal: 16,
70    backgroundColor: 'darkorange'
71  },
72});
73
74export default RepositoryItem
75module.exports = function(api) {
76  api.cache(true);
77  return {
78    presets: ['babel-preset-expo'],
79  };
80};
81const RepositoryList = () =&gt; {
82  return (
83    &lt;FlatList
84      data={repositories}
85      ItemSeparatorComponent={ItemSeparator}
86      renderItem={({ item }) =&gt; &lt;RepositoryItem repository={item}/&gt; }
87    /&gt;
88  );
89};
90const RepositoryList = () =&gt; {
91      return (
92        &lt;FlatList
93          data={repositories}
94          ItemSeparatorComponent={ItemSeparator}
95          renderItem={({ item: repository }) =&gt; &lt;RepositoryItem repository={repository}/&gt; }
96        /&gt;
97      );
98    };
99

Source https://stackoverflow.com/questions/65706686

QUESTION

How to use container exits immediately after startup in docker?

Asked 2020-Oct-29 at 01:31

As everyone knows, we can use docker start [dockerID] to start a closed container.

But, If this container exits immediately after startup. What should I do?

For example, I have a MySQL container, it runs without any problems. But the system is down. At next time I start this container. It tell me a file is worry so that this container immediately exit.
Now I want to delete this file, but this container can not be activated, so I can't enter this container to delete this file. What should I do?

And if I want to open bash in this state container, What should I do?

ANSWER

Answered 2020-Oct-29 at 01:31

Delete the container and launch a new one.

1docker rm dockerID
2docker run --name dockerID ... mysql:5.7
3

Containers are generally treated as disposable; there are times you're required to delete and recreate a container (to change some networking or environment options; to upgrade to a newer version of the underlying image). The flip side of this is that containers' state is generally stored outside the container filesystem itself (you probably have a docker run -v or Docker Compose volumes: option) so it will survive deleting and recreating the container. I almost never use docker start.

Creating a new container gets you around the limitations of docker start:

  • If the container exits immediately but you don't know why, docker run or docker-compose up it without the -d option, so it prints its logs to the console

  • If you want to run a different command (like an interactive shell) as the main container command, you can do it the same as any other container,

1docker rm dockerID
2docker run --name dockerID ... mysql:5.7
3docker run --rm -it -v ...:/var/lib/mysql/data mysql:5.6 sh
4docker-compose run db sh
5
  • If the actual problem can be fixed with an environment variable or other setting, you can add that to the startup-time configuration, since you're already recreating the container

  • Source https://stackoverflow.com/questions/64583059

    QUESTION

    How to append data to existing data object

    Asked 2020-Jun-09 at 15:12

    Im currently using redux to manage my state. The scenario is as such , Upon successful creation of a new object , i would like to append the response data into my existing state container as i don't wish to make a new API call to render it.

    initial State:
    1const initialState = {
    2workflowobject:{},
    3

    };

    SAGA:
    1const initialState = {
    2workflowobject:{},
    3export function* workerCreateTransitionApproval(action) {
    4const data = yield call(() =&gt; axiosInstance.post(`/transition-approval-meta/create/`, action.data))
    5yield put({ type: &quot;STORE_WORKFLOW_DATA&quot;, payload: data.data.data, fetch: 'workflowobject' , label: 'transition_approvals'})
    6

    }

    So over here , i upon recieving the "signal" so to speak to create a transition approval , i will catch that event and create make an axios post request to my backend , which will then return a response of the transition_approval . I will then store this transition_approval as the payload which i will use later on.

    Reducer
    1const initialState = {
    2workflowobject:{},
    3export function* workerCreateTransitionApproval(action) {
    4const data = yield call(() =&gt; axiosInstance.post(`/transition-approval-meta/create/`, action.data))
    5yield put({ type: &quot;STORE_WORKFLOW_DATA&quot;, payload: data.data.data, fetch: 'workflowobject' , label: 'transition_approvals'})
    6const loadWorkflowObject = (state, action) =&gt; {
    7    return updateObject(state, {
    8        workflowobject: { ...state.workflowobject, [action.label]: action.payload }
    9    })
    10}
    11
    12const storeData = (state, action) =&gt; {
    13    switch (action.fetch) {
    14        case 'workflowobject': return loadWorkflowObject(state, action)
    15    }
    16}
    17
    18const reducer = (state = initialState, action) =&gt; {
    19    switch (action.type) {
    20        case 'STORE_WORKFLOW_DATA': return storeData(state, action);
    21        case 'CLEAR_CLASS_STATES': return clearClassStates(state, action);
    22        case 'CLEAR_OBJECT_STATES': return clearObjectStates(state, action);
    23        default:
    24            return state;
    25    }
    26}
    27
    28export default reducer;
    29

    So in my reducer , it will first go into the case STORE_WORKFLOW_DATA which will then return the reducer function loadWorkflowObject . This is where i wish to 'append' the data back to the state tree.

    The problem

    The tricky part here is that im using this loadWorkflowObject reducer for fetching data too , and im already using the spread operator here.

    The code that i have shown above will override my preexisting data that i have in the transition_approvals object , if possible , i would like to append the data in instead.

    ANSWER

    Answered 2020-Jun-09 at 15:12

    you can do this:

    1const initialState = {
    2workflowobject:{},
    3export function* workerCreateTransitionApproval(action) {
    4const data = yield call(() =&gt; axiosInstance.post(`/transition-approval-meta/create/`, action.data))
    5yield put({ type: &quot;STORE_WORKFLOW_DATA&quot;, payload: data.data.data, fetch: 'workflowobject' , label: 'transition_approvals'})
    6const loadWorkflowObject = (state, action) =&gt; {
    7    return updateObject(state, {
    8        workflowobject: { ...state.workflowobject, [action.label]: action.payload }
    9    })
    10}
    11
    12const storeData = (state, action) =&gt; {
    13    switch (action.fetch) {
    14        case 'workflowobject': return loadWorkflowObject(state, action)
    15    }
    16}
    17
    18const reducer = (state = initialState, action) =&gt; {
    19    switch (action.type) {
    20        case 'STORE_WORKFLOW_DATA': return storeData(state, action);
    21        case 'CLEAR_CLASS_STATES': return clearClassStates(state, action);
    22        case 'CLEAR_OBJECT_STATES': return clearObjectStates(state, action);
    23        default:
    24            return state;
    25    }
    26}
    27
    28export default reducer;
    29const loadWorkflowObject = (state, action) =&gt; {
    30    return updateObject(state, {
    31        workflowobject: { ...state.workflowobject, [action.label]: state. transition_approvals.concat(action.payload) }
    32    })
    33}
    34

    Source https://stackoverflow.com/questions/62285779

    QUESTION

    Blazor server side: refresh gui of after api call

    Asked 2020-May-26 at 12:45

    I write a server-side blazor app. You can create sensors with states (Good, warning, error...) with a little API for reporting new states.

    Now I want to refetch the new state (or all sensors) on a blazor client from the DB if the API gets called.

    I tried to apply "3. State Container" from this guide: https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/

    How I can force the site to refetch the sensors after a API request? The Sensor and Blazor Client are different devices.

    1
    2@inject ISensorData _db
    3
    4
    5&lt;h1&gt;Dashboard&lt;/h1&gt;
    6
    7@if (sensors is null)
    8{
    9    &lt;p&gt;Laden...&lt;/p&gt;
    10}
    11else
    12{
    13    if (sensors.Count == 0)
    14    {
    15        &lt;p&gt;Keine Sensoren vorhanden!&lt;/p&gt;
    16    }
    17    else
    18    {
    19        foreach (SensorModel sensor in sensors)
    20        {
    21            &lt;button class="btn btn-lg @StatusColor(sensor.Status) m-2"&gt;@sensor.Name @sensor.Message&lt;/button&gt;
    22        }
    23    }
    24}
    25
    26@code {
    27
    28    private List&lt;SensorModel&gt; sensors;
    29
    30    protected override async Task OnInitializedAsync()
    31    {
    32        sensors = await _db.GetSensors();
    33    }
    34
    35    private string StatusColor(int status)
    36    {
    37        switch (status)
    38        {
    39            case 0:
    40                return "btn-secondary";
    41            case 1:
    42                return "btn-success";
    43            case 2:
    44                return "btn-warning";
    45            case 3:
    46                return "btn-danger";
    47            default:
    48                return "btn-secondary";
    49        }
    50    }
    51}
    52

    My API

    1
    2@inject ISensorData _db
    3
    4
    5&lt;h1&gt;Dashboard&lt;/h1&gt;
    6
    7@if (sensors is null)
    8{
    9    &lt;p&gt;Laden...&lt;/p&gt;
    10}
    11else
    12{
    13    if (sensors.Count == 0)
    14    {
    15        &lt;p&gt;Keine Sensoren vorhanden!&lt;/p&gt;
    16    }
    17    else
    18    {
    19        foreach (SensorModel sensor in sensors)
    20        {
    21            &lt;button class="btn btn-lg @StatusColor(sensor.Status) m-2"&gt;@sensor.Name @sensor.Message&lt;/button&gt;
    22        }
    23    }
    24}
    25
    26@code {
    27
    28    private List&lt;SensorModel&gt; sensors;
    29
    30    protected override async Task OnInitializedAsync()
    31    {
    32        sensors = await _db.GetSensors();
    33    }
    34
    35    private string StatusColor(int status)
    36    {
    37        switch (status)
    38        {
    39            case 0:
    40                return "btn-secondary";
    41            case 1:
    42                return "btn-success";
    43            case 2:
    44                return "btn-warning";
    45            case 3:
    46                return "btn-danger";
    47            default:
    48                return "btn-secondary";
    49        }
    50    }
    51}
    52        [HttpGet("updateState")]
    53        public async Task&lt;IActionResult&gt; UpdateState(int id, int status, string? message)
    54        {
    55            if (id &lt;= 0)
    56            {
    57                return BadRequest();
    58            }
    59
    60            if (string.IsNullOrEmpty(message))
    61            {
    62                message = "";
    63            }
    64            try
    65            {
    66                await _db.UpdateState(id, status, message);
    67                //Task.Run(async () =&gt; { _dbState.CallRequestRefresh(); });
    68                _dbState.CallRequestRefresh();
    69
    70            }
    71            catch(Exception ex)
    72            {
    73                throw ex;
    74            }
    75
    76            return NoContent();
    77        } enter code here
    78

    My API Controller

    1
    2@inject ISensorData _db
    3
    4
    5&lt;h1&gt;Dashboard&lt;/h1&gt;
    6
    7@if (sensors is null)
    8{
    9    &lt;p&gt;Laden...&lt;/p&gt;
    10}
    11else
    12{
    13    if (sensors.Count == 0)
    14    {
    15        &lt;p&gt;Keine Sensoren vorhanden!&lt;/p&gt;
    16    }
    17    else
    18    {
    19        foreach (SensorModel sensor in sensors)
    20        {
    21            &lt;button class="btn btn-lg @StatusColor(sensor.Status) m-2"&gt;@sensor.Name @sensor.Message&lt;/button&gt;
    22        }
    23    }
    24}
    25
    26@code {
    27
    28    private List&lt;SensorModel&gt; sensors;
    29
    30    protected override async Task OnInitializedAsync()
    31    {
    32        sensors = await _db.GetSensors();
    33    }
    34
    35    private string StatusColor(int status)
    36    {
    37        switch (status)
    38        {
    39            case 0:
    40                return "btn-secondary";
    41            case 1:
    42                return "btn-success";
    43            case 2:
    44                return "btn-warning";
    45            case 3:
    46                return "btn-danger";
    47            default:
    48                return "btn-secondary";
    49        }
    50    }
    51}
    52        [HttpGet("updateState")]
    53        public async Task&lt;IActionResult&gt; UpdateState(int id, int status, string? message)
    54        {
    55            if (id &lt;= 0)
    56            {
    57                return BadRequest();
    58            }
    59
    60            if (string.IsNullOrEmpty(message))
    61            {
    62                message = "";
    63            }
    64            try
    65            {
    66                await _db.UpdateState(id, status, message);
    67                //Task.Run(async () =&gt; { _dbState.CallRequestRefresh(); });
    68                _dbState.CallRequestRefresh();
    69
    70            }
    71            catch(Exception ex)
    72            {
    73                throw ex;
    74            }
    75
    76            return NoContent();
    77        } enter code here
    78        {
    79            _db = db;
    80            NavigationManager = navigationManager;
    81            hubConnection = new HubConnectionBuilder()
    82                .WithUrl("https://localhost:44346/dbRefreshHub")
    83                .Build();
    84
    85            hubConnection.StartAsync();
    86        }
    87

    ANSWER

    Answered 2020-May-26 at 10:28

    I do not entirely understand your question as it is not clearly put, and I may be missing something...

    However, the State Container pattern proposed by you can't serve you in any way. It is intended to manage state for your components, etc. But I believe that you are looking for a way to notify your client side app that the "API gets called", and that it ( the client side app ) should update the new state... Am I right ?

    If yes, then, I believe you can implement the SignalR Client (Microsoft.AspNetCore.SignalR.Client) to do that.

    A while ago, I saw in one of the Blazor previews by Daniel Roths, some code snippet demonstrating how to use SignalR Client in Blazor. Search for it, or perhaps look up the topic in the docs, and see if it can offer you some remedies.

    Hope this works...

    Source https://stackoverflow.com/questions/62019253

    QUESTION

    A fast way to fill contours with the ability to export to a polygon-like format

    Asked 2020-May-09 at 16:19

    I have a number of medical pictures presented on canvas, as an example below.

    knee

    I’m trying to make a tool that allows you to select any area of ​​the image with the tool in the form of an expandable circle, and fill in only that part of it that doesn't go beyond the outline in which the original click pixel was located. A filled outline is drawn on a separate canvas layer.

    Now I use the most common iterative stack implementation of flood fill with variable tolerance (comparison function). You can familiarize yourself with it here. Everything doesn't work very well, especially in pictures where there are no strong contrast differences and in high-resolution images, everything else is pretty slow.

    flood fill result

    I had the idea to create a state container and look for whether the desired filled outline exists there and if so, then just replace the canvas pixel array (though, again, I will have to resort to some additional processing, the canvas pixel array contains 4 channel, while at the output of the algorithm only 1 is obtained and just replacing the content doesn't work, you need to replace each pixel with a pixel divided into 4 channels) instead of a slow flood fill each time. But this approach has one significant problem: memory consumption. As you might guess, a filled outline, especially of a decent resolution alone, can take up quite a lot of space, and their set becomes a real problem of memory consumption.

    It was decided to store the finished contours in the form of polygons and extracting them from the container simply fill them with faster context fill. The algorithm used allows me to output a set of boundaries, but due to the features of the algorithm, this array is unordered and connecting the vertices in this order, we get only a partially filled outline (right picture). Is there a way to sort them in such a way that I could only connect them and get a closed path (the holes that are in the filled outline in the left picture shouldn't be a priori, so we don’t have to worry about them)?

    flood fillresulted path based on boundaries

    Summing up, due to the not-so-best fill job, I think to use a different algorithm / implementation, but I don’t know which one. Here are some of my ideas:

    1. Use a different implementation, for example, a line scanning method. As far as I know, here is one of the fastest and most effective implementations of the algorithm among open sources. Pros: possible efficiency and speed. Cons: I need to somehow convert the result to a polygon, rewrite the algorithm to javascript (probably emscripten, can do it well, but in any case I will have to rewrite a considerable part of the code).

    2. Use a completely different approach.

      a) I don’t know, but maybe the Canny detector can be useful for extracting the polygon. But as far as the use of the program is meant on the client side, it will be unprofitable to extract all the boundaries, it is necessary to figure out how to process only the necessary section, and not the entire picture.

      b) Then, knowing the border, use any sufficiently fast fill algorithm that simply won't go beyond the boundaries found.

    I'll be glad to know about some other ways, and even better to see ready-made implementations in javascript

    UPD:

    For a better understanding, the tool cursor and the expected result of the algorithm are presented below.

    tool cursorexpected result

    ANSWER

    Answered 2020-May-09 at 16:19

    Here is an example with opencv

    Below should work or eventually use the fiddle link provided inside the code snippet

    Of interest: approxPolyDP which may be sufficient for your needs (check Ramer-Douglas-Peucker algorithm)

    1// USE FIDDLE
    2// https://jsfiddle.net/c7xrq1uy/
    3
    4async function loadSomeImage() {
    5  const ctx = document.querySelector('#imageSrc').getContext('2d')
    6  ctx.fillStyle = 'black'
    7  const img = new Image()
    8  img.crossOrigin = ''
    9  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    10
    11  img.onload = () =&gt; {
    12    const imgwidth = img.offsetWidth
    13    const imgheight = img.offsetHeight
    14    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    15  }
    16}
    17
    18function plotPoints(canvas, points, color = 'green', hold = false){
    19  const ctx = canvas.getContext('2d')
    20  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    21  ctx.strokeStyle = color
    22
    23  Object.values(points).forEach(ps =&gt; {
    24    ctx.beginPath()
    25    ctx.moveTo(ps[0].x, ps[0].y)
    26    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    27    ctx.closePath()
    28    ctx.stroke()
    29  })
    30}
    31const binarize = (src, threshold) =&gt; {
    32  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    33  const dst = new cv.Mat()
    34  src.convertTo(dst, cv.CV_8U)
    35  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    36  cv.imshow('binary', dst)
    37  return dst
    38}
    39const flip = src =&gt; {
    40  const dst = new cv.Mat()
    41  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    42  cv.imshow('flip', dst)
    43  return dst
    44}
    45const dilate = (src) =&gt; {
    46  const dst = new cv.Mat()
    47  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    48  let anchor = new cv.Point(-1, -1)
    49  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    50  M.delete()
    51  cv.imshow('dilate', dst)
    52  return dst
    53}
    54const PARAMS = {
    55  threshold: 102,
    56  anchor: { x: 180, y: 180 },
    57  eps: 1e-2
    58}
    59const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    60  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    61}
    62document.querySelector('input[type=range]').onmouseup = e =&gt; {
    63  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    64  dumpParams(PARAMS)
    65  runCv(PARAMS)
    66}
    67document.querySelector('input[type=value]').onchange = e =&gt; {
    68  PARAMS.eps = parseFloat(e.target.value)
    69  dumpParams(PARAMS)
    70  runCv(PARAMS)
    71}
    72document.querySelector('#imageSrc').onclick = e =&gt; {
    73  const rect = e.target.getBoundingClientRect()
    74  PARAMS.anchor = {
    75    x: e.clientX - rect.left,
    76    y: e.clientY - rect.top
    77  }
    78  dumpParams(PARAMS)
    79  runCv(PARAMS)
    80}
    81const contourToPoints = cnt =&gt; {
    82  const arr = []
    83  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    84    let p = {}
    85    p.x = cnt.data32S[j]
    86    p.y = cnt.data32S[j+1]
    87    arr.push(p)
    88  }
    89  return arr
    90}
    91loadSomeImage()
    92dumpParams(PARAMS)
    93let CVREADY
    94const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    95
    96const runCv = async ({ threshold, anchor, eps }) =&gt; {
    97  await cvReady
    98  const canvasFinal = document.querySelector('#final')
    99  const mat = cv.imread(document.querySelector('#imageSrc'))
    100  const binaryImg = binarize(mat, threshold, 'binary')
    101  const blurredImg = dilate(binaryImg)
    102  const flipImg = flip(blurredImg)
    103  var contours = new cv.MatVector()
    104  const hierarchy = new cv.Mat
    105  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    106
    107  const points = {}
    108  let matchingPoints = null
    109  let matchingContour = null
    110  for (let i = 0; i &lt; contours.size(); ++i) {
    111  let minArea = 1e40
    112    const ci = contours.get(i)
    113    points[i] = contourToPoints(ci)
    114    if (anchor) {
    115      const point = new cv.Point(anchor.x, anchor.y)
    116      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    117      const area = cv.contourArea(ci)
    118      if (inside &amp;&amp; area &lt; minArea) {
    119        matchingPoints = points[i]
    120        matchingContour = ci
    121        minArea = area
    122      }
    123    }
    124  }
    125  plotPoints(canvasFinal, points)
    126
    127  if (anchor) {
    128    if (matchingPoints) {
    129      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    130      if (eps) {
    131        const epsilon = eps * cv.arcLength(matchingContour, true)
    132        const approx = new cv.Mat()
    133        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    134        const arr = contourToPoints(approx)
    135        console.log('polygon', arr)
    136        plotPoints(canvasFinal, [arr], 'blue', true)
    137      }
    138    }
    139  }
    140  mat.delete()
    141  contours.delete()
    142  hierarchy.delete()
    143  binaryImg.delete()
    144  blurredImg.delete()
    145  flipImg.delete()
    146}
    147function onOpenCvReady() {
    148  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    149}
    150// just so we can load async script
    151var script = document.createElement('script');
    152script.onload = onOpenCvReady
    153script.src = 'https://docs.opencv.org/master/opencv.js';
    154document.head.appendChild(script)
    1// USE FIDDLE
    2// https://jsfiddle.net/c7xrq1uy/
    3
    4async function loadSomeImage() {
    5  const ctx = document.querySelector('#imageSrc').getContext('2d')
    6  ctx.fillStyle = 'black'
    7  const img = new Image()
    8  img.crossOrigin = ''
    9  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    10
    11  img.onload = () =&gt; {
    12    const imgwidth = img.offsetWidth
    13    const imgheight = img.offsetHeight
    14    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    15  }
    16}
    17
    18function plotPoints(canvas, points, color = 'green', hold = false){
    19  const ctx = canvas.getContext('2d')
    20  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    21  ctx.strokeStyle = color
    22
    23  Object.values(points).forEach(ps =&gt; {
    24    ctx.beginPath()
    25    ctx.moveTo(ps[0].x, ps[0].y)
    26    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    27    ctx.closePath()
    28    ctx.stroke()
    29  })
    30}
    31const binarize = (src, threshold) =&gt; {
    32  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    33  const dst = new cv.Mat()
    34  src.convertTo(dst, cv.CV_8U)
    35  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    36  cv.imshow('binary', dst)
    37  return dst
    38}
    39const flip = src =&gt; {
    40  const dst = new cv.Mat()
    41  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    42  cv.imshow('flip', dst)
    43  return dst
    44}
    45const dilate = (src) =&gt; {
    46  const dst = new cv.Mat()
    47  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    48  let anchor = new cv.Point(-1, -1)
    49  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    50  M.delete()
    51  cv.imshow('dilate', dst)
    52  return dst
    53}
    54const PARAMS = {
    55  threshold: 102,
    56  anchor: { x: 180, y: 180 },
    57  eps: 1e-2
    58}
    59const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    60  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    61}
    62document.querySelector('input[type=range]').onmouseup = e =&gt; {
    63  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    64  dumpParams(PARAMS)
    65  runCv(PARAMS)
    66}
    67document.querySelector('input[type=value]').onchange = e =&gt; {
    68  PARAMS.eps = parseFloat(e.target.value)
    69  dumpParams(PARAMS)
    70  runCv(PARAMS)
    71}
    72document.querySelector('#imageSrc').onclick = e =&gt; {
    73  const rect = e.target.getBoundingClientRect()
    74  PARAMS.anchor = {
    75    x: e.clientX - rect.left,
    76    y: e.clientY - rect.top
    77  }
    78  dumpParams(PARAMS)
    79  runCv(PARAMS)
    80}
    81const contourToPoints = cnt =&gt; {
    82  const arr = []
    83  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    84    let p = {}
    85    p.x = cnt.data32S[j]
    86    p.y = cnt.data32S[j+1]
    87    arr.push(p)
    88  }
    89  return arr
    90}
    91loadSomeImage()
    92dumpParams(PARAMS)
    93let CVREADY
    94const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    95
    96const runCv = async ({ threshold, anchor, eps }) =&gt; {
    97  await cvReady
    98  const canvasFinal = document.querySelector('#final')
    99  const mat = cv.imread(document.querySelector('#imageSrc'))
    100  const binaryImg = binarize(mat, threshold, 'binary')
    101  const blurredImg = dilate(binaryImg)
    102  const flipImg = flip(blurredImg)
    103  var contours = new cv.MatVector()
    104  const hierarchy = new cv.Mat
    105  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    106
    107  const points = {}
    108  let matchingPoints = null
    109  let matchingContour = null
    110  for (let i = 0; i &lt; contours.size(); ++i) {
    111  let minArea = 1e40
    112    const ci = contours.get(i)
    113    points[i] = contourToPoints(ci)
    114    if (anchor) {
    115      const point = new cv.Point(anchor.x, anchor.y)
    116      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    117      const area = cv.contourArea(ci)
    118      if (inside &amp;&amp; area &lt; minArea) {
    119        matchingPoints = points[i]
    120        matchingContour = ci
    121        minArea = area
    122      }
    123    }
    124  }
    125  plotPoints(canvasFinal, points)
    126
    127  if (anchor) {
    128    if (matchingPoints) {
    129      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    130      if (eps) {
    131        const epsilon = eps * cv.arcLength(matchingContour, true)
    132        const approx = new cv.Mat()
    133        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    134        const arr = contourToPoints(approx)
    135        console.log('polygon', arr)
    136        plotPoints(canvasFinal, [arr], 'blue', true)
    137      }
    138    }
    139  }
    140  mat.delete()
    141  contours.delete()
    142  hierarchy.delete()
    143  binaryImg.delete()
    144  blurredImg.delete()
    145  flipImg.delete()
    146}
    147function onOpenCvReady() {
    148  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    149}
    150// just so we can load async script
    151var script = document.createElement('script');
    152script.onload = onOpenCvReady
    153script.src = 'https://docs.opencv.org/master/opencv.js';
    154document.head.appendChild(script)canvas{border: 1px solid black;}
    155  .debug{width: 200px; height: 200px;}
    1// USE FIDDLE
    2// https://jsfiddle.net/c7xrq1uy/
    3
    4async function loadSomeImage() {
    5  const ctx = document.querySelector('#imageSrc').getContext('2d')
    6  ctx.fillStyle = 'black'
    7  const img = new Image()
    8  img.crossOrigin = ''
    9  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    10
    11  img.onload = () =&gt; {
    12    const imgwidth = img.offsetWidth
    13    const imgheight = img.offsetHeight
    14    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    15  }
    16}
    17
    18function plotPoints(canvas, points, color = 'green', hold = false){
    19  const ctx = canvas.getContext('2d')
    20  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    21  ctx.strokeStyle = color
    22
    23  Object.values(points).forEach(ps =&gt; {
    24    ctx.beginPath()
    25    ctx.moveTo(ps[0].x, ps[0].y)
    26    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    27    ctx.closePath()
    28    ctx.stroke()
    29  })
    30}
    31const binarize = (src, threshold) =&gt; {
    32  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    33  const dst = new cv.Mat()
    34  src.convertTo(dst, cv.CV_8U)
    35  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    36  cv.imshow('binary', dst)
    37  return dst
    38}
    39const flip = src =&gt; {
    40  const dst = new cv.Mat()
    41  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    42  cv.imshow('flip', dst)
    43  return dst
    44}
    45const dilate = (src) =&gt; {
    46  const dst = new cv.Mat()
    47  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    48  let anchor = new cv.Point(-1, -1)
    49  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    50  M.delete()
    51  cv.imshow('dilate', dst)
    52  return dst
    53}
    54const PARAMS = {
    55  threshold: 102,
    56  anchor: { x: 180, y: 180 },
    57  eps: 1e-2
    58}
    59const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    60  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    61}
    62document.querySelector('input[type=range]').onmouseup = e =&gt; {
    63  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    64  dumpParams(PARAMS)
    65  runCv(PARAMS)
    66}
    67document.querySelector('input[type=value]').onchange = e =&gt; {
    68  PARAMS.eps = parseFloat(e.target.value)
    69  dumpParams(PARAMS)
    70  runCv(PARAMS)
    71}
    72document.querySelector('#imageSrc').onclick = e =&gt; {
    73  const rect = e.target.getBoundingClientRect()
    74  PARAMS.anchor = {
    75    x: e.clientX - rect.left,
    76    y: e.clientY - rect.top
    77  }
    78  dumpParams(PARAMS)
    79  runCv(PARAMS)
    80}
    81const contourToPoints = cnt =&gt; {
    82  const arr = []
    83  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    84    let p = {}
    85    p.x = cnt.data32S[j]
    86    p.y = cnt.data32S[j+1]
    87    arr.push(p)
    88  }
    89  return arr
    90}
    91loadSomeImage()
    92dumpParams(PARAMS)
    93let CVREADY
    94const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    95
    96const runCv = async ({ threshold, anchor, eps }) =&gt; {
    97  await cvReady
    98  const canvasFinal = document.querySelector('#final')
    99  const mat = cv.imread(document.querySelector('#imageSrc'))
    100  const binaryImg = binarize(mat, threshold, 'binary')
    101  const blurredImg = dilate(binaryImg)
    102  const flipImg = flip(blurredImg)
    103  var contours = new cv.MatVector()
    104  const hierarchy = new cv.Mat
    105  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    106
    107  const points = {}
    108  let matchingPoints = null
    109  let matchingContour = null
    110  for (let i = 0; i &lt; contours.size(); ++i) {
    111  let minArea = 1e40
    112    const ci = contours.get(i)
    113    points[i] = contourToPoints(ci)
    114    if (anchor) {
    115      const point = new cv.Point(anchor.x, anchor.y)
    116      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    117      const area = cv.contourArea(ci)
    118      if (inside &amp;&amp; area &lt; minArea) {
    119        matchingPoints = points[i]
    120        matchingContour = ci
    121        minArea = area
    122      }
    123    }
    124  }
    125  plotPoints(canvasFinal, points)
    126
    127  if (anchor) {
    128    if (matchingPoints) {
    129      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    130      if (eps) {
    131        const epsilon = eps * cv.arcLength(matchingContour, true)
    132        const approx = new cv.Mat()
    133        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    134        const arr = contourToPoints(approx)
    135        console.log('polygon', arr)
    136        plotPoints(canvasFinal, [arr], 'blue', true)
    137      }
    138    }
    139  }
    140  mat.delete()
    141  contours.delete()
    142  hierarchy.delete()
    143  binaryImg.delete()
    144  blurredImg.delete()
    145  flipImg.delete()
    146}
    147function onOpenCvReady() {
    148  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    149}
    150// just so we can load async script
    151var script = document.createElement('script');
    152script.onload = onOpenCvReady
    153script.src = 'https://docs.opencv.org/master/opencv.js';
    154document.head.appendChild(script)canvas{border: 1px solid black;}
    155  .debug{width: 200px; height: 200px;}binarization threeshold&lt;input type="range" min="0" max="100"/&gt;&lt;br/&gt;
    156eps(approxPolyDp) &lt;input type="value" placeholder="0.01"/&gt;&lt;br/&gt;
    157params: &lt;span id="params"&gt;&lt;/span&gt;&lt;br/&gt;
    158&lt;br/&gt;
    159&lt;canvas id="imageSrc" height="400" width="400"/&gt;&lt;/canvas&gt;
    160&lt;canvas id="final" height="400" width="400"&gt;&lt;/canvas&gt;
    161&lt;br/&gt;
    162&lt;canvas class="debug" id="binary" height="400" width="400" title="binary"&gt;&lt;/canvas&gt;
    163&lt;canvas class="debug" id="dilate" height="400" width="400" title="dilate"&gt;&lt;/canvas&gt;
    164&lt;canvas class="debug" id="flip" height="400" width="400" title="flip"&gt;&lt;/canvas&gt;

    ps: polygon is output in the console


    implem with mask

    edit: in below snippet I had more fun and implemented the mask. We may make the snippet [full page] then hover over the first canvas.

    1// USE FIDDLE
    2// https://jsfiddle.net/c7xrq1uy/
    3
    4async function loadSomeImage() {
    5  const ctx = document.querySelector('#imageSrc').getContext('2d')
    6  ctx.fillStyle = 'black'
    7  const img = new Image()
    8  img.crossOrigin = ''
    9  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    10
    11  img.onload = () =&gt; {
    12    const imgwidth = img.offsetWidth
    13    const imgheight = img.offsetHeight
    14    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    15  }
    16}
    17
    18function plotPoints(canvas, points, color = 'green', hold = false){
    19  const ctx = canvas.getContext('2d')
    20  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    21  ctx.strokeStyle = color
    22
    23  Object.values(points).forEach(ps =&gt; {
    24    ctx.beginPath()
    25    ctx.moveTo(ps[0].x, ps[0].y)
    26    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    27    ctx.closePath()
    28    ctx.stroke()
    29  })
    30}
    31const binarize = (src, threshold) =&gt; {
    32  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    33  const dst = new cv.Mat()
    34  src.convertTo(dst, cv.CV_8U)
    35  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    36  cv.imshow('binary', dst)
    37  return dst
    38}
    39const flip = src =&gt; {
    40  const dst = new cv.Mat()
    41  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    42  cv.imshow('flip', dst)
    43  return dst
    44}
    45const dilate = (src) =&gt; {
    46  const dst = new cv.Mat()
    47  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    48  let anchor = new cv.Point(-1, -1)
    49  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    50  M.delete()
    51  cv.imshow('dilate', dst)
    52  return dst
    53}
    54const PARAMS = {
    55  threshold: 102,
    56  anchor: { x: 180, y: 180 },
    57  eps: 1e-2
    58}
    59const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    60  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    61}
    62document.querySelector('input[type=range]').onmouseup = e =&gt; {
    63  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    64  dumpParams(PARAMS)
    65  runCv(PARAMS)
    66}
    67document.querySelector('input[type=value]').onchange = e =&gt; {
    68  PARAMS.eps = parseFloat(e.target.value)
    69  dumpParams(PARAMS)
    70  runCv(PARAMS)
    71}
    72document.querySelector('#imageSrc').onclick = e =&gt; {
    73  const rect = e.target.getBoundingClientRect()
    74  PARAMS.anchor = {
    75    x: e.clientX - rect.left,
    76    y: e.clientY - rect.top
    77  }
    78  dumpParams(PARAMS)
    79  runCv(PARAMS)
    80}
    81const contourToPoints = cnt =&gt; {
    82  const arr = []
    83  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    84    let p = {}
    85    p.x = cnt.data32S[j]
    86    p.y = cnt.data32S[j+1]
    87    arr.push(p)
    88  }
    89  return arr
    90}
    91loadSomeImage()
    92dumpParams(PARAMS)
    93let CVREADY
    94const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    95
    96const runCv = async ({ threshold, anchor, eps }) =&gt; {
    97  await cvReady
    98  const canvasFinal = document.querySelector('#final')
    99  const mat = cv.imread(document.querySelector('#imageSrc'))
    100  const binaryImg = binarize(mat, threshold, 'binary')
    101  const blurredImg = dilate(binaryImg)
    102  const flipImg = flip(blurredImg)
    103  var contours = new cv.MatVector()
    104  const hierarchy = new cv.Mat
    105  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    106
    107  const points = {}
    108  let matchingPoints = null
    109  let matchingContour = null
    110  for (let i = 0; i &lt; contours.size(); ++i) {
    111  let minArea = 1e40
    112    const ci = contours.get(i)
    113    points[i] = contourToPoints(ci)
    114    if (anchor) {
    115      const point = new cv.Point(anchor.x, anchor.y)
    116      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    117      const area = cv.contourArea(ci)
    118      if (inside &amp;&amp; area &lt; minArea) {
    119        matchingPoints = points[i]
    120        matchingContour = ci
    121        minArea = area
    122      }
    123    }
    124  }
    125  plotPoints(canvasFinal, points)
    126
    127  if (anchor) {
    128    if (matchingPoints) {
    129      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    130      if (eps) {
    131        const epsilon = eps * cv.arcLength(matchingContour, true)
    132        const approx = new cv.Mat()
    133        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    134        const arr = contourToPoints(approx)
    135        console.log('polygon', arr)
    136        plotPoints(canvasFinal, [arr], 'blue', true)
    137      }
    138    }
    139  }
    140  mat.delete()
    141  contours.delete()
    142  hierarchy.delete()
    143  binaryImg.delete()
    144  blurredImg.delete()
    145  flipImg.delete()
    146}
    147function onOpenCvReady() {
    148  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    149}
    150// just so we can load async script
    151var script = document.createElement('script');
    152script.onload = onOpenCvReady
    153script.src = 'https://docs.opencv.org/master/opencv.js';
    154document.head.appendChild(script)canvas{border: 1px solid black;}
    155  .debug{width: 200px; height: 200px;}binarization threeshold&lt;input type="range" min="0" max="100"/&gt;&lt;br/&gt;
    156eps(approxPolyDp) &lt;input type="value" placeholder="0.01"/&gt;&lt;br/&gt;
    157params: &lt;span id="params"&gt;&lt;/span&gt;&lt;br/&gt;
    158&lt;br/&gt;
    159&lt;canvas id="imageSrc" height="400" width="400"/&gt;&lt;/canvas&gt;
    160&lt;canvas id="final" height="400" width="400"&gt;&lt;/canvas&gt;
    161&lt;br/&gt;
    162&lt;canvas class="debug" id="binary" height="400" width="400" title="binary"&gt;&lt;/canvas&gt;
    163&lt;canvas class="debug" id="dilate" height="400" width="400" title="dilate"&gt;&lt;/canvas&gt;
    164&lt;canvas class="debug" id="flip" height="400" width="400" title="flip"&gt;&lt;/canvas&gt;// USE FIDDLE
    165// https://jsfiddle.net/c7xrq1uy/
    166
    167async function loadSomeImage() {
    168  const ctx = document.querySelector('#imageSrc').getContext('2d')
    169  ctx.fillStyle = 'black'
    170  const img = new Image()
    171  img.crossOrigin = ''
    172  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    173
    174  img.onload = () =&gt; {
    175    const imgwidth = img.offsetWidth
    176    const imgheight = img.offsetHeight
    177    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    178  }
    179}
    180
    181function plotPoints(canvas, points, color = 'green', hold = false){
    182  const ctx = canvas.getContext('2d')
    183  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    184  ctx.strokeStyle = color
    185
    186  Object.values(points).forEach(ps =&gt; {
    187    ctx.beginPath()
    188    ctx.moveTo(ps[0].x, ps[0].y)
    189    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    190    ctx.closePath()
    191    ctx.stroke()
    192  })
    193}
    194const binarize = (src, threshold) =&gt; {
    195  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    196  const dst = new cv.Mat()
    197  src.convertTo(dst, cv.CV_8U)
    198  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    199  cv.imshow('binary', dst)
    200  return dst
    201}
    202const flip = src =&gt; {
    203  const dst = new cv.Mat()
    204  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    205  cv.imshow('flip', dst)
    206  return dst
    207}
    208const dilate = (src) =&gt; {
    209  const dst = new cv.Mat()
    210  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    211  let anchor = new cv.Point(-1, -1)
    212  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    213  M.delete()
    214  cv.imshow('dilate', dst)
    215  return dst
    216}
    217const PARAMS = {
    218  threshold: 102,
    219  anchor: { x: 180, y: 180 },
    220  eps: 1e-2,
    221  radius: 50
    222}
    223const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    224  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    225}
    226document.querySelector('input[type=range]').onmouseup = e =&gt; {
    227  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    228  dumpParams(PARAMS)
    229  runCv(PARAMS)
    230}
    231document.querySelector('input[type=value]').onchange = e =&gt; {
    232  PARAMS.eps = parseFloat(e.target.value)
    233  dumpParams(PARAMS)
    234  runCv(PARAMS)
    235}
    236document.querySelector('#imageSrc').onclick = e =&gt; {
    237  const rect = e.target.getBoundingClientRect()
    238  PARAMS.anchor = {
    239    x: e.clientX - rect.left,
    240    y: e.clientY - rect.top
    241  }
    242  dumpParams(PARAMS)
    243  runCv(PARAMS)
    244}
    245// sorry for the globals, keep code simple
    246let DST = null
    247let MATCHING_CONTOUR = null
    248let DEBOUNCE = 0
    249document.querySelector('#imageSrc').onmousemove = e =&gt; {
    250  if (Date.now() - DEBOUNCE &lt; 100) return
    251  if (!MATCHING_CONTOUR || !DST) { return }
    252  const rect = e.target.getBoundingClientRect()
    253  DEBOUNCE = Date.now()
    254  const x = e.clientX - rect.left
    255  const y = e.clientY - rect.top
    256  const dst = DST.clone()
    257  plotIntersectingMask(dst, MATCHING_CONTOUR, { anchor: { x, y }, radius: PARAMS.radius })
    258  dst.delete()
    259}
    260const contourToPoints = cnt =&gt; {
    261  const arr = []
    262  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    263    let p = {}
    264    p.x = cnt.data32S[j]
    265    p.y = cnt.data32S[j+1]
    266    arr.push(p)
    267  }
    268  return arr
    269}
    270const plotIntersectingMask = (dst, cnt, { anchor, radius }) =&gt; {
    271  const { width, height } = dst.size()
    272  
    273  const contourMask = new cv.Mat.zeros(height, width, dst.type())
    274  const matVec = new cv.MatVector()
    275  matVec.push_back(cnt)
    276  cv.fillPoly(contourMask, matVec, [255, 255, 255, 255])
    277
    278  const userCircle = new cv.Mat.zeros(height, width, dst.type())
    279  cv.circle(userCircle, new cv.Point(anchor.x, anchor.y), radius, [255, 128, 68, 255], -2)
    280
    281  const commonMask = new cv.Mat.zeros(height, width, dst.type())
    282  cv.bitwise_and(contourMask, userCircle, commonMask)
    283  
    284  userCircle.copyTo(dst, commonMask)
    285  cv.imshow('final', dst)
    286
    287  commonMask.delete()
    288  matVec.delete()
    289  contourMask.delete()
    290  userCircle.delete()
    291}
    292loadSomeImage()
    293dumpParams(PARAMS)
    294let CVREADY
    295const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    296
    297const runCv = async ({ threshold, anchor, eps, radius }) =&gt; {
    298  await cvReady
    299  const canvasFinal = document.querySelector('#final')
    300  const mat = cv.imread(document.querySelector('#imageSrc'))
    301  const binaryImg = binarize(mat, threshold, 'binary')
    302  const blurredImg = dilate(binaryImg)
    303  const flipImg = flip(blurredImg)
    304  var contours = new cv.MatVector()
    305  const hierarchy = new cv.Mat
    306  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    307
    308  const points = {}
    309  let matchingPoints = null
    310  let matchingContour = null
    311  for (let i = 0; i &lt; contours.size(); ++i) {
    312  let minArea = 1e40
    313    const ci = contours.get(i)
    314    points[i] = contourToPoints(ci)
    315    if (anchor) {
    316      const point = new cv.Point(anchor.x, anchor.y)
    317      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    318      const area = cv.contourArea(ci)
    319      if (inside &amp;&amp; area &lt; minArea) {
    320        matchingPoints = points[i]
    321        matchingContour = ci
    322        minArea = area
    323      }
    324    }
    325  }
    326  plotPoints(canvasFinal, points)
    327
    328  if (anchor) {
    329    if (matchingPoints) {
    330      MATCHING_CONTOUR = matchingContour
    331      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    332      if (eps) {
    333        const epsilon = eps * cv.arcLength(matchingContour, true)
    334        const approx = new cv.Mat()
    335        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    336        const arr = contourToPoints(approx)
    337        //console.log('polygon', arr)
    338        plotPoints(canvasFinal, [arr], 'blue', true)
    339
    340        if (DST) DST.delete()
    341        DST = cv.imread(document.querySelector('#final'))
    342      }
    343    }
    344  }
    345  mat.delete()
    346  contours.delete()
    347  hierarchy.delete()
    348  binaryImg.delete()
    349  blurredImg.delete()
    350  flipImg.delete()
    351}
    352function onOpenCvReady() {
    353  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    354}
    355// just so we can load async script
    356var script = document.createElement('script');
    357script.onload = onOpenCvReady
    358script.src = 'https://docs.opencv.org/master/opencv.js';
    359document.head.appendChild(script)
    1// USE FIDDLE
    2// https://jsfiddle.net/c7xrq1uy/
    3
    4async function loadSomeImage() {
    5  const ctx = document.querySelector('#imageSrc').getContext('2d')
    6  ctx.fillStyle = 'black'
    7  const img = new Image()
    8  img.crossOrigin = ''
    9  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    10
    11  img.onload = () =&gt; {
    12    const imgwidth = img.offsetWidth
    13    const imgheight = img.offsetHeight
    14    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    15  }
    16}
    17
    18function plotPoints(canvas, points, color = 'green', hold = false){
    19  const ctx = canvas.getContext('2d')
    20  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    21  ctx.strokeStyle = color
    22
    23  Object.values(points).forEach(ps =&gt; {
    24    ctx.beginPath()
    25    ctx.moveTo(ps[0].x, ps[0].y)
    26    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    27    ctx.closePath()
    28    ctx.stroke()
    29  })
    30}
    31const binarize = (src, threshold) =&gt; {
    32  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    33  const dst = new cv.Mat()
    34  src.convertTo(dst, cv.CV_8U)
    35  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    36  cv.imshow('binary', dst)
    37  return dst
    38}
    39const flip = src =&gt; {
    40  const dst = new cv.Mat()
    41  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    42  cv.imshow('flip', dst)
    43  return dst
    44}
    45const dilate = (src) =&gt; {
    46  const dst = new cv.Mat()
    47  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    48  let anchor = new cv.Point(-1, -1)
    49  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    50  M.delete()
    51  cv.imshow('dilate', dst)
    52  return dst
    53}
    54const PARAMS = {
    55  threshold: 102,
    56  anchor: { x: 180, y: 180 },
    57  eps: 1e-2
    58}
    59const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    60  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    61}
    62document.querySelector('input[type=range]').onmouseup = e =&gt; {
    63  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    64  dumpParams(PARAMS)
    65  runCv(PARAMS)
    66}
    67document.querySelector('input[type=value]').onchange = e =&gt; {
    68  PARAMS.eps = parseFloat(e.target.value)
    69  dumpParams(PARAMS)
    70  runCv(PARAMS)
    71}
    72document.querySelector('#imageSrc').onclick = e =&gt; {
    73  const rect = e.target.getBoundingClientRect()
    74  PARAMS.anchor = {
    75    x: e.clientX - rect.left,
    76    y: e.clientY - rect.top
    77  }
    78  dumpParams(PARAMS)
    79  runCv(PARAMS)
    80}
    81const contourToPoints = cnt =&gt; {
    82  const arr = []
    83  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    84    let p = {}
    85    p.x = cnt.data32S[j]
    86    p.y = cnt.data32S[j+1]
    87    arr.push(p)
    88  }
    89  return arr
    90}
    91loadSomeImage()
    92dumpParams(PARAMS)
    93let CVREADY
    94const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    95
    96const runCv = async ({ threshold, anchor, eps }) =&gt; {
    97  await cvReady
    98  const canvasFinal = document.querySelector('#final')
    99  const mat = cv.imread(document.querySelector('#imageSrc'))
    100  const binaryImg = binarize(mat, threshold, 'binary')
    101  const blurredImg = dilate(binaryImg)
    102  const flipImg = flip(blurredImg)
    103  var contours = new cv.MatVector()
    104  const hierarchy = new cv.Mat
    105  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    106
    107  const points = {}
    108  let matchingPoints = null
    109  let matchingContour = null
    110  for (let i = 0; i &lt; contours.size(); ++i) {
    111  let minArea = 1e40
    112    const ci = contours.get(i)
    113    points[i] = contourToPoints(ci)
    114    if (anchor) {
    115      const point = new cv.Point(anchor.x, anchor.y)
    116      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    117      const area = cv.contourArea(ci)
    118      if (inside &amp;&amp; area &lt; minArea) {
    119        matchingPoints = points[i]
    120        matchingContour = ci
    121        minArea = area
    122      }
    123    }
    124  }
    125  plotPoints(canvasFinal, points)
    126
    127  if (anchor) {
    128    if (matchingPoints) {
    129      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    130      if (eps) {
    131        const epsilon = eps * cv.arcLength(matchingContour, true)
    132        const approx = new cv.Mat()
    133        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    134        const arr = contourToPoints(approx)
    135        console.log('polygon', arr)
    136        plotPoints(canvasFinal, [arr], 'blue', true)
    137      }
    138    }
    139  }
    140  mat.delete()
    141  contours.delete()
    142  hierarchy.delete()
    143  binaryImg.delete()
    144  blurredImg.delete()
    145  flipImg.delete()
    146}
    147function onOpenCvReady() {
    148  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    149}
    150// just so we can load async script
    151var script = document.createElement('script');
    152script.onload = onOpenCvReady
    153script.src = 'https://docs.opencv.org/master/opencv.js';
    154document.head.appendChild(script)canvas{border: 1px solid black;}
    155  .debug{width: 200px; height: 200px;}binarization threeshold&lt;input type="range" min="0" max="100"/&gt;&lt;br/&gt;
    156eps(approxPolyDp) &lt;input type="value" placeholder="0.01"/&gt;&lt;br/&gt;
    157params: &lt;span id="params"&gt;&lt;/span&gt;&lt;br/&gt;
    158&lt;br/&gt;
    159&lt;canvas id="imageSrc" height="400" width="400"/&gt;&lt;/canvas&gt;
    160&lt;canvas id="final" height="400" width="400"&gt;&lt;/canvas&gt;
    161&lt;br/&gt;
    162&lt;canvas class="debug" id="binary" height="400" width="400" title="binary"&gt;&lt;/canvas&gt;
    163&lt;canvas class="debug" id="dilate" height="400" width="400" title="dilate"&gt;&lt;/canvas&gt;
    164&lt;canvas class="debug" id="flip" height="400" width="400" title="flip"&gt;&lt;/canvas&gt;// USE FIDDLE
    165// https://jsfiddle.net/c7xrq1uy/
    166
    167async function loadSomeImage() {
    168  const ctx = document.querySelector('#imageSrc').getContext('2d')
    169  ctx.fillStyle = 'black'
    170  const img = new Image()
    171  img.crossOrigin = ''
    172  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    173
    174  img.onload = () =&gt; {
    175    const imgwidth = img.offsetWidth
    176    const imgheight = img.offsetHeight
    177    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    178  }
    179}
    180
    181function plotPoints(canvas, points, color = 'green', hold = false){
    182  const ctx = canvas.getContext('2d')
    183  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    184  ctx.strokeStyle = color
    185
    186  Object.values(points).forEach(ps =&gt; {
    187    ctx.beginPath()
    188    ctx.moveTo(ps[0].x, ps[0].y)
    189    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    190    ctx.closePath()
    191    ctx.stroke()
    192  })
    193}
    194const binarize = (src, threshold) =&gt; {
    195  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    196  const dst = new cv.Mat()
    197  src.convertTo(dst, cv.CV_8U)
    198  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    199  cv.imshow('binary', dst)
    200  return dst
    201}
    202const flip = src =&gt; {
    203  const dst = new cv.Mat()
    204  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    205  cv.imshow('flip', dst)
    206  return dst
    207}
    208const dilate = (src) =&gt; {
    209  const dst = new cv.Mat()
    210  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    211  let anchor = new cv.Point(-1, -1)
    212  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    213  M.delete()
    214  cv.imshow('dilate', dst)
    215  return dst
    216}
    217const PARAMS = {
    218  threshold: 102,
    219  anchor: { x: 180, y: 180 },
    220  eps: 1e-2,
    221  radius: 50
    222}
    223const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    224  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    225}
    226document.querySelector('input[type=range]').onmouseup = e =&gt; {
    227  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    228  dumpParams(PARAMS)
    229  runCv(PARAMS)
    230}
    231document.querySelector('input[type=value]').onchange = e =&gt; {
    232  PARAMS.eps = parseFloat(e.target.value)
    233  dumpParams(PARAMS)
    234  runCv(PARAMS)
    235}
    236document.querySelector('#imageSrc').onclick = e =&gt; {
    237  const rect = e.target.getBoundingClientRect()
    238  PARAMS.anchor = {
    239    x: e.clientX - rect.left,
    240    y: e.clientY - rect.top
    241  }
    242  dumpParams(PARAMS)
    243  runCv(PARAMS)
    244}
    245// sorry for the globals, keep code simple
    246let DST = null
    247let MATCHING_CONTOUR = null
    248let DEBOUNCE = 0
    249document.querySelector('#imageSrc').onmousemove = e =&gt; {
    250  if (Date.now() - DEBOUNCE &lt; 100) return
    251  if (!MATCHING_CONTOUR || !DST) { return }
    252  const rect = e.target.getBoundingClientRect()
    253  DEBOUNCE = Date.now()
    254  const x = e.clientX - rect.left
    255  const y = e.clientY - rect.top
    256  const dst = DST.clone()
    257  plotIntersectingMask(dst, MATCHING_CONTOUR, { anchor: { x, y }, radius: PARAMS.radius })
    258  dst.delete()
    259}
    260const contourToPoints = cnt =&gt; {
    261  const arr = []
    262  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    263    let p = {}
    264    p.x = cnt.data32S[j]
    265    p.y = cnt.data32S[j+1]
    266    arr.push(p)
    267  }
    268  return arr
    269}
    270const plotIntersectingMask = (dst, cnt, { anchor, radius }) =&gt; {
    271  const { width, height } = dst.size()
    272  
    273  const contourMask = new cv.Mat.zeros(height, width, dst.type())
    274  const matVec = new cv.MatVector()
    275  matVec.push_back(cnt)
    276  cv.fillPoly(contourMask, matVec, [255, 255, 255, 255])
    277
    278  const userCircle = new cv.Mat.zeros(height, width, dst.type())
    279  cv.circle(userCircle, new cv.Point(anchor.x, anchor.y), radius, [255, 128, 68, 255], -2)
    280
    281  const commonMask = new cv.Mat.zeros(height, width, dst.type())
    282  cv.bitwise_and(contourMask, userCircle, commonMask)
    283  
    284  userCircle.copyTo(dst, commonMask)
    285  cv.imshow('final', dst)
    286
    287  commonMask.delete()
    288  matVec.delete()
    289  contourMask.delete()
    290  userCircle.delete()
    291}
    292loadSomeImage()
    293dumpParams(PARAMS)
    294let CVREADY
    295const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    296
    297const runCv = async ({ threshold, anchor, eps, radius }) =&gt; {
    298  await cvReady
    299  const canvasFinal = document.querySelector('#final')
    300  const mat = cv.imread(document.querySelector('#imageSrc'))
    301  const binaryImg = binarize(mat, threshold, 'binary')
    302  const blurredImg = dilate(binaryImg)
    303  const flipImg = flip(blurredImg)
    304  var contours = new cv.MatVector()
    305  const hierarchy = new cv.Mat
    306  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    307
    308  const points = {}
    309  let matchingPoints = null
    310  let matchingContour = null
    311  for (let i = 0; i &lt; contours.size(); ++i) {
    312  let minArea = 1e40
    313    const ci = contours.get(i)
    314    points[i] = contourToPoints(ci)
    315    if (anchor) {
    316      const point = new cv.Point(anchor.x, anchor.y)
    317      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    318      const area = cv.contourArea(ci)
    319      if (inside &amp;&amp; area &lt; minArea) {
    320        matchingPoints = points[i]
    321        matchingContour = ci
    322        minArea = area
    323      }
    324    }
    325  }
    326  plotPoints(canvasFinal, points)
    327
    328  if (anchor) {
    329    if (matchingPoints) {
    330      MATCHING_CONTOUR = matchingContour
    331      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    332      if (eps) {
    333        const epsilon = eps * cv.arcLength(matchingContour, true)
    334        const approx = new cv.Mat()
    335        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    336        const arr = contourToPoints(approx)
    337        //console.log('polygon', arr)
    338        plotPoints(canvasFinal, [arr], 'blue', true)
    339
    340        if (DST) DST.delete()
    341        DST = cv.imread(document.querySelector('#final'))
    342      }
    343    }
    344  }
    345  mat.delete()
    346  contours.delete()
    347  hierarchy.delete()
    348  binaryImg.delete()
    349  blurredImg.delete()
    350  flipImg.delete()
    351}
    352function onOpenCvReady() {
    353  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    354}
    355// just so we can load async script
    356var script = document.createElement('script');
    357script.onload = onOpenCvReady
    358script.src = 'https://docs.opencv.org/master/opencv.js';
    359document.head.appendChild(script)  canvas{border: 1px solid black;}
    360  .debug{width: 200px; height: 200px;}
    361  #imageSrc{cursor: pointer;}
    1// USE FIDDLE
    2// https://jsfiddle.net/c7xrq1uy/
    3
    4async function loadSomeImage() {
    5  const ctx = document.querySelector('#imageSrc').getContext('2d')
    6  ctx.fillStyle = 'black'
    7  const img = new Image()
    8  img.crossOrigin = ''
    9  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    10
    11  img.onload = () =&gt; {
    12    const imgwidth = img.offsetWidth
    13    const imgheight = img.offsetHeight
    14    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    15  }
    16}
    17
    18function plotPoints(canvas, points, color = 'green', hold = false){
    19  const ctx = canvas.getContext('2d')
    20  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    21  ctx.strokeStyle = color
    22
    23  Object.values(points).forEach(ps =&gt; {
    24    ctx.beginPath()
    25    ctx.moveTo(ps[0].x, ps[0].y)
    26    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    27    ctx.closePath()
    28    ctx.stroke()
    29  })
    30}
    31const binarize = (src, threshold) =&gt; {
    32  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    33  const dst = new cv.Mat()
    34  src.convertTo(dst, cv.CV_8U)
    35  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    36  cv.imshow('binary', dst)
    37  return dst
    38}
    39const flip = src =&gt; {
    40  const dst = new cv.Mat()
    41  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    42  cv.imshow('flip', dst)
    43  return dst
    44}
    45const dilate = (src) =&gt; {
    46  const dst = new cv.Mat()
    47  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    48  let anchor = new cv.Point(-1, -1)
    49  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    50  M.delete()
    51  cv.imshow('dilate', dst)
    52  return dst
    53}
    54const PARAMS = {
    55  threshold: 102,
    56  anchor: { x: 180, y: 180 },
    57  eps: 1e-2
    58}
    59const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    60  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    61}
    62document.querySelector('input[type=range]').onmouseup = e =&gt; {
    63  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    64  dumpParams(PARAMS)
    65  runCv(PARAMS)
    66}
    67document.querySelector('input[type=value]').onchange = e =&gt; {
    68  PARAMS.eps = parseFloat(e.target.value)
    69  dumpParams(PARAMS)
    70  runCv(PARAMS)
    71}
    72document.querySelector('#imageSrc').onclick = e =&gt; {
    73  const rect = e.target.getBoundingClientRect()
    74  PARAMS.anchor = {
    75    x: e.clientX - rect.left,
    76    y: e.clientY - rect.top
    77  }
    78  dumpParams(PARAMS)
    79  runCv(PARAMS)
    80}
    81const contourToPoints = cnt =&gt; {
    82  const arr = []
    83  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    84    let p = {}
    85    p.x = cnt.data32S[j]
    86    p.y = cnt.data32S[j+1]
    87    arr.push(p)
    88  }
    89  return arr
    90}
    91loadSomeImage()
    92dumpParams(PARAMS)
    93let CVREADY
    94const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    95
    96const runCv = async ({ threshold, anchor, eps }) =&gt; {
    97  await cvReady
    98  const canvasFinal = document.querySelector('#final')
    99  const mat = cv.imread(document.querySelector('#imageSrc'))
    100  const binaryImg = binarize(mat, threshold, 'binary')
    101  const blurredImg = dilate(binaryImg)
    102  const flipImg = flip(blurredImg)
    103  var contours = new cv.MatVector()
    104  const hierarchy = new cv.Mat
    105  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    106
    107  const points = {}
    108  let matchingPoints = null
    109  let matchingContour = null
    110  for (let i = 0; i &lt; contours.size(); ++i) {
    111  let minArea = 1e40
    112    const ci = contours.get(i)
    113    points[i] = contourToPoints(ci)
    114    if (anchor) {
    115      const point = new cv.Point(anchor.x, anchor.y)
    116      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    117      const area = cv.contourArea(ci)
    118      if (inside &amp;&amp; area &lt; minArea) {
    119        matchingPoints = points[i]
    120        matchingContour = ci
    121        minArea = area
    122      }
    123    }
    124  }
    125  plotPoints(canvasFinal, points)
    126
    127  if (anchor) {
    128    if (matchingPoints) {
    129      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    130      if (eps) {
    131        const epsilon = eps * cv.arcLength(matchingContour, true)
    132        const approx = new cv.Mat()
    133        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    134        const arr = contourToPoints(approx)
    135        console.log('polygon', arr)
    136        plotPoints(canvasFinal, [arr], 'blue', true)
    137      }
    138    }
    139  }
    140  mat.delete()
    141  contours.delete()
    142  hierarchy.delete()
    143  binaryImg.delete()
    144  blurredImg.delete()
    145  flipImg.delete()
    146}
    147function onOpenCvReady() {
    148  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    149}
    150// just so we can load async script
    151var script = document.createElement('script');
    152script.onload = onOpenCvReady
    153script.src = 'https://docs.opencv.org/master/opencv.js';
    154document.head.appendChild(script)canvas{border: 1px solid black;}
    155  .debug{width: 200px; height: 200px;}binarization threeshold&lt;input type="range" min="0" max="100"/&gt;&lt;br/&gt;
    156eps(approxPolyDp) &lt;input type="value" placeholder="0.01"/&gt;&lt;br/&gt;
    157params: &lt;span id="params"&gt;&lt;/span&gt;&lt;br/&gt;
    158&lt;br/&gt;
    159&lt;canvas id="imageSrc" height="400" width="400"/&gt;&lt;/canvas&gt;
    160&lt;canvas id="final" height="400" width="400"&gt;&lt;/canvas&gt;
    161&lt;br/&gt;
    162&lt;canvas class="debug" id="binary" height="400" width="400" title="binary"&gt;&lt;/canvas&gt;
    163&lt;canvas class="debug" id="dilate" height="400" width="400" title="dilate"&gt;&lt;/canvas&gt;
    164&lt;canvas class="debug" id="flip" height="400" width="400" title="flip"&gt;&lt;/canvas&gt;// USE FIDDLE
    165// https://jsfiddle.net/c7xrq1uy/
    166
    167async function loadSomeImage() {
    168  const ctx = document.querySelector('#imageSrc').getContext('2d')
    169  ctx.fillStyle = 'black'
    170  const img = new Image()
    171  img.crossOrigin = ''
    172  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'
    173
    174  img.onload = () =&gt; {
    175    const imgwidth = img.offsetWidth
    176    const imgheight = img.offsetHeight
    177    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
    178  }
    179}
    180
    181function plotPoints(canvas, points, color = 'green', hold = false){
    182  const ctx = canvas.getContext('2d')
    183  !hold &amp;&amp; ctx.clearRect(0, 0, 400, 400)
    184  ctx.strokeStyle = color
    185
    186  Object.values(points).forEach(ps =&gt; {
    187    ctx.beginPath()
    188    ctx.moveTo(ps[0].x, ps[0].y)
    189    ps.slice(1).forEach(({ x, y }) =&gt; ctx.lineTo(x,y))
    190    ctx.closePath()
    191    ctx.stroke()
    192  })
    193}
    194const binarize = (src, threshold) =&gt; {
    195  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
    196  const dst = new cv.Mat()
    197  src.convertTo(dst, cv.CV_8U)
    198  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
    199  cv.imshow('binary', dst)
    200  return dst
    201}
    202const flip = src =&gt; {
    203  const dst = new cv.Mat()
    204  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
    205  cv.imshow('flip', dst)
    206  return dst
    207}
    208const dilate = (src) =&gt; {
    209  const dst = new cv.Mat()
    210  let M = cv.Mat.ones(3, 3, cv.CV_8U)
    211  let anchor = new cv.Point(-1, -1)
    212  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
    213  M.delete()
    214  cv.imshow('dilate', dst)
    215  return dst
    216}
    217const PARAMS = {
    218  threshold: 102,
    219  anchor: { x: 180, y: 180 },
    220  eps: 1e-2,
    221  radius: 50
    222}
    223const dumpParams = ({ threshold, anchor, eps }) =&gt; {
    224  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
    225}
    226document.querySelector('input[type=range]').onmouseup = e =&gt; {
    227  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
    228  dumpParams(PARAMS)
    229  runCv(PARAMS)
    230}
    231document.querySelector('input[type=value]').onchange = e =&gt; {
    232  PARAMS.eps = parseFloat(e.target.value)
    233  dumpParams(PARAMS)
    234  runCv(PARAMS)
    235}
    236document.querySelector('#imageSrc').onclick = e =&gt; {
    237  const rect = e.target.getBoundingClientRect()
    238  PARAMS.anchor = {
    239    x: e.clientX - rect.left,
    240    y: e.clientY - rect.top
    241  }
    242  dumpParams(PARAMS)
    243  runCv(PARAMS)
    244}
    245// sorry for the globals, keep code simple
    246let DST = null
    247let MATCHING_CONTOUR = null
    248let DEBOUNCE = 0
    249document.querySelector('#imageSrc').onmousemove = e =&gt; {
    250  if (Date.now() - DEBOUNCE &lt; 100) return
    251  if (!MATCHING_CONTOUR || !DST) { return }
    252  const rect = e.target.getBoundingClientRect()
    253  DEBOUNCE = Date.now()
    254  const x = e.clientX - rect.left
    255  const y = e.clientY - rect.top
    256  const dst = DST.clone()
    257  plotIntersectingMask(dst, MATCHING_CONTOUR, { anchor: { x, y }, radius: PARAMS.radius })
    258  dst.delete()
    259}
    260const contourToPoints = cnt =&gt; {
    261  const arr = []
    262  for (let j = 0; j &lt; cnt.data32S.length; j += 2){
    263    let p = {}
    264    p.x = cnt.data32S[j]
    265    p.y = cnt.data32S[j+1]
    266    arr.push(p)
    267  }
    268  return arr
    269}
    270const plotIntersectingMask = (dst, cnt, { anchor, radius }) =&gt; {
    271  const { width, height } = dst.size()
    272  
    273  const contourMask = new cv.Mat.zeros(height, width, dst.type())
    274  const matVec = new cv.MatVector()
    275  matVec.push_back(cnt)
    276  cv.fillPoly(contourMask, matVec, [255, 255, 255, 255])
    277
    278  const userCircle = new cv.Mat.zeros(height, width, dst.type())
    279  cv.circle(userCircle, new cv.Point(anchor.x, anchor.y), radius, [255, 128, 68, 255], -2)
    280
    281  const commonMask = new cv.Mat.zeros(height, width, dst.type())
    282  cv.bitwise_and(contourMask, userCircle, commonMask)
    283  
    284  userCircle.copyTo(dst, commonMask)
    285  cv.imshow('final', dst)
    286
    287  commonMask.delete()
    288  matVec.delete()
    289  contourMask.delete()
    290  userCircle.delete()
    291}
    292loadSomeImage()
    293dumpParams(PARAMS)
    294let CVREADY
    295const cvReady = new Promise((resolve, reject) =&gt; CVREADY = resolve)
    296
    297const runCv = async ({ threshold, anchor, eps, radius }) =&gt; {
    298  await cvReady
    299  const canvasFinal = document.querySelector('#final')
    300  const mat = cv.imread(document.querySelector('#imageSrc'))
    301  const binaryImg = binarize(mat, threshold, 'binary')
    302  const blurredImg = dilate(binaryImg)
    303  const flipImg = flip(blurredImg)
    304  var contours = new cv.MatVector()
    305  const hierarchy = new cv.Mat
    306  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    307
    308  const points = {}
    309  let matchingPoints = null
    310  let matchingContour = null
    311  for (let i = 0; i &lt; contours.size(); ++i) {
    312  let minArea = 1e40
    313    const ci = contours.get(i)
    314    points[i] = contourToPoints(ci)
    315    if (anchor) {
    316      const point = new cv.Point(anchor.x, anchor.y)
    317      const inside = cv.pointPolygonTest(ci, point, false) &gt;= 1
    318      const area = cv.contourArea(ci)
    319      if (inside &amp;&amp; area &lt; minArea) {
    320        matchingPoints = points[i]
    321        matchingContour = ci
    322        minArea = area
    323      }
    324    }
    325  }
    326  plotPoints(canvasFinal, points)
    327
    328  if (anchor) {
    329    if (matchingPoints) {
    330      MATCHING_CONTOUR = matchingContour
    331      plotPoints(canvasFinal, [matchingPoints], 'red', true)
    332      if (eps) {
    333        const epsilon = eps * cv.arcLength(matchingContour, true)
    334        const approx = new cv.Mat()
    335        cv.approxPolyDP(matchingContour, approx, epsilon, true)
    336        const arr = contourToPoints(approx)
    337        //console.log('polygon', arr)
    338        plotPoints(canvasFinal, [arr], 'blue', true)
    339
    340        if (DST) DST.delete()
    341        DST = cv.imread(document.querySelector('#final'))
    342      }
    343    }
    344  }
    345  mat.delete()
    346  contours.delete()
    347  hierarchy.delete()
    348  binaryImg.delete()
    349  blurredImg.delete()
    350  flipImg.delete()
    351}
    352function onOpenCvReady() {
    353  cv['onRuntimeInitialized'] = () =&gt; {console.log('cvready'); CVREADY(); runCv(PARAMS)}
    354}
    355// just so we can load async script
    356var script = document.createElement('script');
    357script.onload = onOpenCvReady
    358script.src = 'https://docs.opencv.org/master/opencv.js';
    359document.head.appendChild(script)  canvas{border: 1px solid black;}
    360  .debug{width: 200px; height: 200px;}
    361  #imageSrc{cursor: pointer;}binarization threeshold&lt;input type="range" min="0" max="100"/&gt;&lt;br/&gt;
    362eps(approxPolyDp) &lt;input type="value" placeholder="0.01"/&gt;&lt;br/&gt;
    363params: &lt;span id="params"&gt;&lt;/span&gt;&lt;br/&gt;
    364&lt;br/&gt;
    365&lt;canvas id="imageSrc" height="400" width="400"/&gt;&lt;/canvas&gt;
    366&lt;canvas id="final" height="400" width="400"&gt;&lt;/canvas&gt;
    367&lt;br/&gt;
    368&lt;canvas class="debug" id="binary" height="400" width="400" title="binary"&gt;&lt;/canvas&gt;
    369&lt;canvas class="debug" id="dilate" height="400" width="400" title="dilate"&gt;&lt;/canvas&gt;
    370&lt;canvas class="debug" id="flip" height="400" width="400" title="flip"&gt;&lt;/canvas&gt;

    Source https://stackoverflow.com/questions/61685407

    Community Discussions contain sources that include Stack Exchange Network

    Tutorials and Learning Resources in State Container

    Tutorials and Learning Resources are not available at this moment for State Container

    Share this Page

    share link

    Get latest updates on State Container