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
by reduxjs typescript
57862 NOASSERTION
Predictable state container for JavaScript apps
by vuejs javascript
27468 MIT
🗃️ Centralized State Management for Vue.js.
by mobxjs typescript
25031 MIT
Simple, scalable state management.
by redux-saga javascript
22138 NOASSERTION
An alternative side effect model for Redux apps
by Meituan-Dianping javascript
20382 MIT
基于 Vue.js 的小程序开发框架,从底层支持 Vue.js 语法和构建工具体系。
by verekia javascript
18568 MIT
🛠️⚡ Step-by-step tutorial to build a modern JavaScript stack.
by reduxjs javascript
16590 MIT
Thunk middleware for Redux
by JacksonTian javascript
16584
前端技能汇总 Frontend Knowledge Structure
by dvajs javascript
15740 MIT
🌱 React and redux based, lightweight and elm-style framework. (Inspired by elm and choo)
Trending New libraries in State Container
by vueuse javascript
1514 MIT
🎩 Creates Universal Library for Vue 2 & 3
by antfu typescript
1451 MIT
📲 On-demand components auto importing for Vue
by zenghongtu typescript
1415 MIT
👻在你的桌面放一个萌妹子,多一点趣味😏~(支持Mac、Win和Linux)
by buqiyuan typescript
893 MIT
基于vue-cli/vite + vue3.0 + ant-design-vue2.0 + typescript hooks 的基础后台管理系统模板 RBAC的权限系统, JSON Schema动态表单,动态表格,漂亮锁屏界面
by intlify typescript
781 MIT
Vue I18n for Vue 3
by newbee-ltd java
725 GPL-3.0
🔥 🎉新蜂商城前后端分离版本-后端API源码
by cool-team-official css
680 MIT
cool-admin一个很酷的后台权限管理框架,模块化、插件化、CRUD极速开发,永久开源免费,基于midway.js 2.0、typeorm、mysql、jwt、element-ui、vuex、vue-router、vue等构建
by jiechud javascript
595
Taro_Mall是一款多端开源在线商城应用程序,后台是基于litemall基础上进行开发,前端采用Taro框架编写,现已全部完成小程序和h5移动端,后续会对APP,淘宝,头条,百度小程序进行适配。Taro_Mall已经完成了 litemall 前端的所有功能
by rtk-incubator typescript
580 MIT
Data fetching and caching addon for Redux Toolkit
Top Authors in State Container
1
13 Libraries
2610
2
13 Libraries
399
3
12 Libraries
1756
4
11 Libraries
36499
5
10 Libraries
97579
6
10 Libraries
284
7
9 Libraries
3958
8
9 Libraries
290
9
9 Libraries
704
10
8 Libraries
65
1
13 Libraries
2610
2
13 Libraries
399
3
12 Libraries
1756
4
11 Libraries
36499
5
10 Libraries
97579
6
10 Libraries
284
7
9 Libraries
3958
8
9 Libraries
290
9
9 Libraries
704
10
8 Libraries
65
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:32So 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 aState
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 onActions
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 onActions
. - The
dispatch
function that consumers of this hook use is almost impossible to misuse. The first argument only accepts valid actions fromActions
keys and the second argument is either not allowed in the case of theActions
type beingnull
or is forced to conform to theActions
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:05PS - 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 = "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}
120type GenericActionHandler<S, P> = (state: S, payload: P) => Partial<S> | void;
121
122type Config<S, AH extends Record<string, GenericActionHandler<S, any>>> = {
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 = "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}
120type GenericActionHandler<S, P> = (state: S, payload: P) => Partial<S> | void;
121
122type Config<S, AH extends Record<string, GenericActionHandler<S, any>>> = {
123...
124type Actions<AH> = {
125 [K in keyof AH]: AH[K] extends GenericActionHandler<any, infer P>
126 ? (unknown extends P ? never : P )
127 : never;
128};
129
130type TypePayloadPair<AH> = {
131 [K in keyof AH]: Actions<AH>[K] extends null | undefined
132 ? [K]
133 : [K, Actions<AH>[K]];
134}[keyof AH];
135
136type Dispatch<AH> = (...args: TypePayloadPair<AH>) => 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 = "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}
120type GenericActionHandler<S, P> = (state: S, payload: P) => Partial<S> | void;
121
122type Config<S, AH extends Record<string, GenericActionHandler<S, any>>> = {
123...
124type Actions<AH> = {
125 [K in keyof AH]: AH[K] extends GenericActionHandler<any, infer P>
126 ? (unknown extends P ? never : P )
127 : never;
128};
129
130type TypePayloadPair<AH> = {
131 [K in keyof AH]: Actions<AH>[K] extends null | undefined
132 ? [K]
133 : [K, Actions<AH>[K]];
134}[keyof AH];
135
136type Dispatch<AH> = (...args: TypePayloadPair<AH>) => void;
137type StateContainer<S, AH> = {
138 Provider: React.FC<{defaultState?: S}>;
139 useContextState: () => {
140 state: S;
141 dispatch: Dispatch<AH>;
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 = "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}
120type GenericActionHandler<S, P> = (state: S, payload: P) => Partial<S> | void;
121
122type Config<S, AH extends Record<string, GenericActionHandler<S, any>>> = {
123...
124type Actions<AH> = {
125 [K in keyof AH]: AH[K] extends GenericActionHandler<any, infer P>
126 ? (unknown extends P ? never : P )
127 : never;
128};
129
130type TypePayloadPair<AH> = {
131 [K in keyof AH]: Actions<AH>[K] extends null | undefined
132 ? [K]
133 : [K, Actions<AH>[K]];
134}[keyof AH];
135
136type Dispatch<AH> = (...args: TypePayloadPair<AH>) => void;
137type StateContainer<S, AH> = {
138 Provider: React.FC<{defaultState?: S}>;
139 useContextState: () => {
140 state: S;
141 dispatch: Dispatch<AH>;
142 }
143}
144export default function createStateContainer<
145 S,
146 AH extends Record<string, GenericActionHandler<S, unknown>>
147>({
148 name,
149 initialState,
150 actionHandlers,
151}: Config<S, AH>): StateContainer<S, AH> {
152
153 const Context = React.createContext<
154 {
155 state: S;
156 dispatch: Dispatch<AH>
157 } | undefined
158 >(undefined);
159
160 function reducer(state: S, [type, payload]: TypePayloadPair<AH>): 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<AH> = (...args) => reducerDispatch(args);
170
171 return (
172 <Context.Provider value={{ state, dispatch }}>
173 {children}
174 </Context.Provider>
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 = "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}
120type GenericActionHandler<S, P> = (state: S, payload: P) => Partial<S> | void;
121
122type Config<S, AH extends Record<string, GenericActionHandler<S, any>>> = {
123...
124type Actions<AH> = {
125 [K in keyof AH]: AH[K] extends GenericActionHandler<any, infer P>
126 ? (unknown extends P ? never : P )
127 : never;
128};
129
130type TypePayloadPair<AH> = {
131 [K in keyof AH]: Actions<AH>[K] extends null | undefined
132 ? [K]
133 : [K, Actions<AH>[K]];
134}[keyof AH];
135
136type Dispatch<AH> = (...args: TypePayloadPair<AH>) => void;
137type StateContainer<S, AH> = {
138 Provider: React.FC<{defaultState?: S}>;
139 useContextState: () => {
140 state: S;
141 dispatch: Dispatch<AH>;
142 }
143}
144export default function createStateContainer<
145 S,
146 AH extends Record<string, GenericActionHandler<S, unknown>>
147>({
148 name,
149 initialState,
150 actionHandlers,
151}: Config<S, AH>): StateContainer<S, AH> {
152
153 const Context = React.createContext<
154 {
155 state: S;
156 dispatch: Dispatch<AH>
157 } | undefined
158 >(undefined);
159
160 function reducer(state: S, [type, payload]: TypePayloadPair<AH>): 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<AH> = (...args) => reducerDispatch(args);
170
171 return (
172 <Context.Provider value={{ state, dispatch }}>
173 {children}
174 </Context.Provider>
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 "./StateContainer";
192
193export const {
194 Provider: MyStateProvider,
195 useContextState: useMyState
196} = createStateContainer({
197 name: "MyState",
198 initialState: {
199 nums: [] as number[]
200 },
201 actionHandlers: {
202 RESET_NUMS: () => ({ nums: [] }),
203 ADD_NUM: ({ nums }, num: number) => {
204 nums.push(num);
205 return { nums };
206 },
207 SET_NUMS: ({}, nums: number[]) => ({ nums })
208 }
209});
210
QUESTION
How we can call a method from child with State container Blazor Webassembly
Asked 2021-Sep-08 at 16:11I 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() => 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() => OnChange?.Invoke();
21}
22 @inject AppState AppState
23<p>============================</p>
24<h3>#1 Child</h3>
25<p>send from Father :<b>@AppState?.MyMsgToChildren</b> </p>
26
27<div class="col-sm-6">
28 <button @onclick="SetMessage">Send To Parent</button>
29
30</div>
31<p>============================</p>
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("Message From Child #1");
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() => OnChange?.Invoke();
21}
22 @inject AppState AppState
23<p>============================</p>
24<h3>#1 Child</h3>
25<p>send from Father :<b>@AppState?.MyMsgToChildren</b> </p>
26
27<div class="col-sm-6">
28 <button @onclick="SetMessage">Send To Parent</button>
29
30</div>
31<p>============================</p>
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("Message From Child #1");
46 }
47
48}
49@page "/State"
50
51@inject AppState AppState
52<p>============================</p>
53<h3>Parent</h3>
54<p>send from child:<b>@AppState?.MyMessage</b> </p>
55<div class="col-sm-6">
56 <button @onclick="SendMsgTochildren">Send To Parent</button>
57</div>
58<ChildComponent></ChildComponent>
59<Child2Component></Child2Component>
60
61@code {
62
63 public string MsgToChildren { get; private set; } = "Hi, Im your father - ";
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:04I 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() => OnChange?.Invoke();
21}
22 @inject AppState AppState
23<p>============================</p>
24<h3>#1 Child</h3>
25<p>send from Father :<b>@AppState?.MyMsgToChildren</b> </p>
26
27<div class="col-sm-6">
28 <button @onclick="SetMessage">Send To Parent</button>
29
30</div>
31<p>============================</p>
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("Message From Child #1");
46 }
47
48}
49@page "/State"
50
51@inject AppState AppState
52<p>============================</p>
53<h3>Parent</h3>
54<p>send from child:<b>@AppState?.MyMessage</b> </p>
55<div class="col-sm-6">
56 <button @onclick="SendMsgTochildren">Send To Parent</button>
57</div>
58<ChildComponent></ChildComponent>
59<Child2Component></Child2Component>
60
61@code {
62
63 public string MsgToChildren { get; private set; } = "Hi, Im your father - ";
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<int> 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() => OnChange?.Invoke();
21}
22 @inject AppState AppState
23<p>============================</p>
24<h3>#1 Child</h3>
25<p>send from Father :<b>@AppState?.MyMsgToChildren</b> </p>
26
27<div class="col-sm-6">
28 <button @onclick="SetMessage">Send To Parent</button>
29
30</div>
31<p>============================</p>
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("Message From Child #1");
46 }
47
48}
49@page "/State"
50
51@inject AppState AppState
52<p>============================</p>
53<h3>Parent</h3>
54<p>send from child:<b>@AppState?.MyMessage</b> </p>
55<div class="col-sm-6">
56 <button @onclick="SendMsgTochildren">Send To Parent</button>
57</div>
58<ChildComponent></ChildComponent>
59<Child2Component></Child2Component>
60
61@code {
62
63 public string MsgToChildren { get; private set; } = "Hi, Im your father - ";
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<int> OnCounterChanged { get; set; }
90 }
91 @inject AppState AppState;
92@page "/grandparent"
93<h1>Counter Value from Grandparent : @Counter</h1>
94 <Parent/>
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() => OnChange?.Invoke();
21}
22 @inject AppState AppState
23<p>============================</p>
24<h3>#1 Child</h3>
25<p>send from Father :<b>@AppState?.MyMsgToChildren</b> </p>
26
27<div class="col-sm-6">
28 <button @onclick="SetMessage">Send To Parent</button>
29
30</div>
31<p>============================</p>
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("Message From Child #1");
46 }
47
48}
49@page "/State"
50
51@inject AppState AppState
52<p>============================</p>
53<h3>Parent</h3>
54<p>send from child:<b>@AppState?.MyMessage</b> </p>
55<div class="col-sm-6">
56 <button @onclick="SendMsgTochildren">Send To Parent</button>
57</div>
58<ChildComponent></ChildComponent>
59<Child2Component></Child2Component>
60
61@code {
62
63 public string MsgToChildren { get; private set; } = "Hi, Im your father - ";
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<int> OnCounterChanged { get; set; }
90 }
91 @inject AppState AppState;
92@page "/grandparent"
93<h1>Counter Value from Grandparent : @Counter</h1>
94 <Parent/>
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<h1>Counter Value from Parent : @Counter</h1>
114<Child />
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<Child />
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() => OnChange?.Invoke();
21}
22 @inject AppState AppState
23<p>============================</p>
24<h3>#1 Child</h3>
25<p>send from Father :<b>@AppState?.MyMsgToChildren</b> </p>
26
27<div class="col-sm-6">
28 <button @onclick="SetMessage">Send To Parent</button>
29
30</div>
31<p>============================</p>
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("Message From Child #1");
46 }
47
48}
49@page "/State"
50
51@inject AppState AppState
52<p>============================</p>
53<h3>Parent</h3>
54<p>send from child:<b>@AppState?.MyMessage</b> </p>
55<div class="col-sm-6">
56 <button @onclick="SendMsgTochildren">Send To Parent</button>
57</div>
58<ChildComponent></ChildComponent>
59<Child2Component></Child2Component>
60
61@code {
62
63 public string MsgToChildren { get; private set; } = "Hi, Im your father - ";
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<int> OnCounterChanged { get; set; }
90 }
91 @inject AppState AppState;
92@page "/grandparent"
93<h1>Counter Value from Grandparent : @Counter</h1>
94 <Parent/>
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<h1>Counter Value from Parent : @Counter</h1>
114<Child />
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<Child />
133@inject AppState AppState;
134<h1>Counter Value from child : @Counter</h1>
135<button class="btn btn-primary" @onclick="UpdateCounter"> Update Counter</button>
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)
QUESTION
Bounded value not updated when modified by JsInterop
Asked 2021-Aug-30 at 10:07I'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<bool> WriteInput(string elementId, string letter)
8 {
9 return await _js.InvokeAsync<bool>("WriteInput", 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<bool> WriteInput(string elementId, string letter)
8 {
9 return await _js.InvokeAsync<bool>("WriteInput", 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<bool> WriteInput(string elementId, string letter)
8 {
9 return await _js.InvokeAsync<bool>("WriteInput", elementId, letter);
10 }
11private async void OnClick(string letter)
12 {
13 await _FocusService.WriteInput(KeyBoard.Id, letter);
14 }
15<input @bind-Value="ViewModel.Username" OnFocus='()=>KeyBoard.Id="loginusername"' Id="loginusername" />
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:07I'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<bool> WriteInput(string elementId, string letter)
8 {
9 return await _js.InvokeAsync<bool>("WriteInput", elementId, letter);
10 }
11private async void OnClick(string letter)
12 {
13 await _FocusService.WriteInput(KeyBoard.Id, letter);
14 }
15<input @bind-Value="ViewModel.Username" OnFocus='()=>KeyBoard.Id="loginusername"' Id="loginusername" />
16@page "/"
17<h3>SoftKeyboard</h3>
18<div class="m-1 p-2">
19 First Name: <input @bind-value="_model.FirstName" id="FirstNameField" />
20</div>
21<div class="m-1 p-2">
22 Surname: <input @bind-value="_model.Surname" id="SurnameField" />
23</div>
24<div class="m-1 p-2">
25 UserName: <input @bind-value="_model.UserName" id="UserNameField" />
26</div>
27
28<div class="m-1 p-2">
29 Focus Control: <select @onchange="FieldSelector">
30 <option value="FirstNameField">First Name</option>
31 <option value="SurnameField">Surname</option>
32 <option value="UserNameField">User Name</option>
33 </select>
34</div>
35
36<div class="m-1 p-2">
37 <button class="btn btn-dark ms-1" @onclick="((e) => SoftClick('A'))">A</button>
38 <button class="btn btn-dark ms-1" @onclick="((e) => SoftClick('S'))">S</button>
39 <button class="btn btn-dark ms-1" @onclick="((e) => SoftClick('D'))">D</button>
40 <button class="btn btn-dark ms-1" @onclick="((e) => SoftClick('F'))">F</button>
41 <button class="btn btn-dark ms-1" @onclick="((e) => SoftClick('K'))">K</button>
42</div>
43
44<div class="m-1 p-2">
45 <div>First Name: @_model.FirstName</div>
46 <div>Surname: @_model.Surname</div>
47 <div>User Name: @_model.UserName</div>
48</div>
49
50@code {
51
52 [Inject] IJSRuntime _js { get; set; }
53
54 async void SoftClick(char key)
55 {
56 await _js.InvokeAsync<bool>("WriteInput", selectedField, key);
57 }
58
59 void FieldSelector(ChangeEventArgs e)
60 {
61 selectedField = e.Value.ToString();
62 }
63
64 private string selectedField = "FirstNameField";
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
QUESTION
Blazor components: How to communicate from grandchild to child to parent or grandchild to parent
Asked 2021-Jul-11 at 17:21In 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<tbody>
2 @foreach (var documentNumber in DocumentNumbers)
3 {
4 <DisplayDocumentNumber DocumentNumber="documentNumber" /> // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5 }
6</tbody>
7
Here's the DisplayDocumentNumber component:
1<tbody>
2 @foreach (var documentNumber in DocumentNumbers)
3 {
4 <DisplayDocumentNumber DocumentNumber="documentNumber" /> // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5 }
6</tbody>
7public partial class DisplayDocumentNumber : ComponentBase
8{
9 [Parameter]
10 public DocumentNumberDto DocumentNumber { get; set; }
11 [Parameter]
12 public EventCallback<bool> 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<tbody>
2 @foreach (var documentNumber in DocumentNumbers)
3 {
4 <DisplayDocumentNumber DocumentNumber="documentNumber" /> // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5 }
6</tbody>
7public partial class DisplayDocumentNumber : ComponentBase
8{
9 [Parameter]
10 public DocumentNumberDto DocumentNumber { get; set; }
11 [Parameter]
12 public EventCallback<bool> OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14<tr>
15 <td>@DocumentNumber.Column1Name</td>
16 <td>@DocumentNumber.Column2Name</td>
17 ...etc
18 <td><button class="btn btn-primary table-btn" @onclick="@(() => ShowEditDocumentNumberDialog(DocumentNumber))">Edit</button></td>
19 <UpdateDocumentNumberDialog @ref="UpdateDocNumDialog" DocumentNumberForUpdating="DocumentNumber" DocumentNumberUpdatedEventCallback="@UpdateDocumentNumberDialog_OnDialogClose"></UpdateDocumentNumberDialog>
20</tr>
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:14I 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<tbody>
2 @foreach (var documentNumber in DocumentNumbers)
3 {
4 <DisplayDocumentNumber DocumentNumber="documentNumber" /> // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5 }
6</tbody>
7public partial class DisplayDocumentNumber : ComponentBase
8{
9 [Parameter]
10 public DocumentNumberDto DocumentNumber { get; set; }
11 [Parameter]
12 public EventCallback<bool> OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14<tr>
15 <td>@DocumentNumber.Column1Name</td>
16 <td>@DocumentNumber.Column2Name</td>
17 ...etc
18 <td><button class="btn btn-primary table-btn" @onclick="@(() => ShowEditDocumentNumberDialog(DocumentNumber))">Edit</button></td>
19 <UpdateDocumentNumberDialog @ref="UpdateDocNumDialog" DocumentNumberForUpdating="DocumentNumber" DocumentNumberUpdatedEventCallback="@UpdateDocumentNumberDialog_OnDialogClose"></UpdateDocumentNumberDialog>
20</tr>
21<CascadingValue Value="this">
22 Body of page, somewhere in there including <Grandchild/>
23</CascadingValue>
24
25@code{
26 async Task DoSomething(){
27 }
28}
29
Grandchild.razor
1<tbody>
2 @foreach (var documentNumber in DocumentNumbers)
3 {
4 <DisplayDocumentNumber DocumentNumber="documentNumber" /> // DocumentNumber parameter in DisplayDocumentNumber receives the data from the documentNumber local var
5 }
6</tbody>
7public partial class DisplayDocumentNumber : ComponentBase
8{
9 [Parameter]
10 public DocumentNumberDto DocumentNumber { get; set; }
11 [Parameter]
12 public EventCallback<bool> OnDocumentNumberUpdatedEventCallback { get; set; }
13}
14<tr>
15 <td>@DocumentNumber.Column1Name</td>
16 <td>@DocumentNumber.Column2Name</td>
17 ...etc
18 <td><button class="btn btn-primary table-btn" @onclick="@(() => ShowEditDocumentNumberDialog(DocumentNumber))">Edit</button></td>
19 <UpdateDocumentNumberDialog @ref="UpdateDocNumDialog" DocumentNumberForUpdating="DocumentNumber" DocumentNumberUpdatedEventCallback="@UpdateDocumentNumberDialog_OnDialogClose"></UpdateDocumentNumberDialog>
20</tr>
21<CascadingValue Value="this">
22 Body of page, somewhere in there including <Grandchild/>
23</CascadingValue>
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
QUESTION
Is there a way to trigger two consecutive events in Blazor with a single click?
Asked 2021-Mar-16 at 20:47Solved - 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 => card.BackGroundColor == "yellow").ToList();
8
9 var potentialSet = _mapper.Map<List<SetCardUiModel>, List<SetCard>>(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 => card.BackGroundColor == "yellow").ToList();
8
9 var potentialSet = _mapper.Map<List<SetCardUiModel>, List<SetCard>>(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="() => { 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 => card.BackGroundColor == "yellow").ToList();
8
9 var potentialSet = _mapper.Map<List<SetCardUiModel>, List<SetCard>>(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="() => { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }
19<div class="cardsContainer">
20 @for (int i = 0; i < numberOfCardsVisible; i++)
21 {
22 var index = i;
23 <div class="card @lineClass" style="background-color:@uniqueCardCombinations[index].BackGroundColor;
24 border-color:@uniqueCardCombinations[index].BorderColor;"
25 @onclick="() => { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }">
26 @for (int j = 0; j < uniqueCardCombinations[i].Count; j++)
27 {
28 <div class="@uniqueCardCombinations[i].Shape @uniqueCardCombinations[i].Color @uniqueCardCombinations[i].Border"></div>
29 }
30 </div>
31 }
32</div>
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 => card.BackGroundColor == "yellow").ToList();
8
9 var potentialSet = _mapper.Map<List<SetCardUiModel>, List<SetCard>>(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="() => { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }
19<div class="cardsContainer">
20 @for (int i = 0; i < numberOfCardsVisible; i++)
21 {
22 var index = i;
23 <div class="card @lineClass" style="background-color:@uniqueCardCombinations[index].BackGroundColor;
24 border-color:@uniqueCardCombinations[index].BorderColor;"
25 @onclick="() => { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }">
26 @for (int j = 0; j < uniqueCardCombinations[i].Count; j++)
27 {
28 <div class="@uniqueCardCombinations[i].Shape @uniqueCardCombinations[i].Color @uniqueCardCombinations[i].Border"></div>
29 }
30 </div>
31 }
32</div>
33private void ProcessSelection(SetCardUiModel setCard)
34 {
35 numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
36
37 if (numberOfSelected == 3)
38 {
39 var setSubmission = uniqueCardCombinations.Where(card => card.BackGroundColor == "yellow").ToList();
40 var potentialSet = _mapper.Map<List<SetCardUiModel>, List<SetCard>>(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 => card.BorderColor == "red").ToList();
53 var countGreenBorders = uniqueCardCombinations.Count(card => card.BorderColor == "green");
54
55 // The while ensures that the 'ProcessSelection' function, which is also called, has run first
56 while (redBorderedCards.Count == 0 && countGreenBorders == 0)
57 {
58 Thread.Sleep(125);
59 redBorderedCards = uniqueCardCombinations.Where(card => card.BorderColor == "red").ToList();
60 countGreenBorders = uniqueCardCombinations.Count(card => card.BorderColor == "green");
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 => card.BackGroundColor == "yellow");
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 > settings.numberOfCardsVisible ? 3 : 0;
75
76 EnsureSetExistsOnField();
77 }
78 else
79 {
80 foreach (var card in redBorderedCards)
81 {
82 card.BackGroundColor = "white";
83 card.BorderColor = "black";
84 }
85 }
86 };
87 }
88
ANSWER
Answered 2021-Mar-15 at 21:02Do 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 fromSystem.Timers
. - On Elapsed of the timer call
ProcessSetReplacement
.
QUESTION
Flatlist undefined is not an object React-native
Asked 2021-Jan-13 at 17:23I 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 = () => <View style={styles.separator} />;
38
39const RepositoryList = () => {
40 return (
41 <FlatList
42 data={repositories}
43 ItemSeparatorComponent={ItemSeparator}
44 renderItem={({repository}) => <RepositoryItem repository={repository}/> }
45 />
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 = () => <View style={styles.separator} />;
38
39const RepositoryList = () => {
40 return (
41 <FlatList
42 data={repositories}
43 ItemSeparatorComponent={ItemSeparator}
44 renderItem={({repository}) => <RepositoryItem repository={repository}/> }
45 />
46 );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) => {
54 return(
55 <View style={styles.item}>
56 <Text>Full name:{repository.fullName}</Text>
57 <Text>Description:{repository.description}</Text>
58 <Text>Language:{repository.language}</Text>
59 <Text>Stars:{repository.stargazersCount}</Text>
60 <Text>Forks:{repository.forksCount}</Text>
61 <Text>Reviews:{repository.reviewCount}</Text>
62 <Text>Rating:{repository.ratingAverage}</Text>
63 </View>
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 = () => <View style={styles.separator} />;
38
39const RepositoryList = () => {
40 return (
41 <FlatList
42 data={repositories}
43 ItemSeparatorComponent={ItemSeparator}
44 renderItem={({repository}) => <RepositoryItem repository={repository}/> }
45 />
46 );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) => {
54 return(
55 <View style={styles.item}>
56 <Text>Full name:{repository.fullName}</Text>
57 <Text>Description:{repository.description}</Text>
58 <Text>Language:{repository.language}</Text>
59 <Text>Stars:{repository.stargazersCount}</Text>
60 <Text>Forks:{repository.forksCount}</Text>
61 <Text>Reviews:{repository.reviewCount}</Text>
62 <Text>Rating:{repository.ratingAverage}</Text>
63 </View>
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:23I 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 = () => <View style={styles.separator} />;
38
39const RepositoryList = () => {
40 return (
41 <FlatList
42 data={repositories}
43 ItemSeparatorComponent={ItemSeparator}
44 renderItem={({repository}) => <RepositoryItem repository={repository}/> }
45 />
46 );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) => {
54 return(
55 <View style={styles.item}>
56 <Text>Full name:{repository.fullName}</Text>
57 <Text>Description:{repository.description}</Text>
58 <Text>Language:{repository.language}</Text>
59 <Text>Stars:{repository.stargazersCount}</Text>
60 <Text>Forks:{repository.forksCount}</Text>
61 <Text>Reviews:{repository.reviewCount}</Text>
62 <Text>Rating:{repository.ratingAverage}</Text>
63 </View>
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 = () => {
82 return (
83 <FlatList
84 data={repositories}
85 ItemSeparatorComponent={ItemSeparator}
86 renderItem={({ item }) => <RepositoryItem repository={item}/> }
87 />
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 = () => <View style={styles.separator} />;
38
39const RepositoryList = () => {
40 return (
41 <FlatList
42 data={repositories}
43 ItemSeparatorComponent={ItemSeparator}
44 renderItem={({repository}) => <RepositoryItem repository={repository}/> }
45 />
46 );
47};
48
49export default RepositoryList
50import React from 'react'
51import {View, Text, StyleSheet} from 'react-native'
52
53const RepositoryItem = ({repository}) => {
54 return(
55 <View style={styles.item}>
56 <Text>Full name:{repository.fullName}</Text>
57 <Text>Description:{repository.description}</Text>
58 <Text>Language:{repository.language}</Text>
59 <Text>Stars:{repository.stargazersCount}</Text>
60 <Text>Forks:{repository.forksCount}</Text>
61 <Text>Reviews:{repository.reviewCount}</Text>
62 <Text>Rating:{repository.ratingAverage}</Text>
63 </View>
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 = () => {
82 return (
83 <FlatList
84 data={repositories}
85 ItemSeparatorComponent={ItemSeparator}
86 renderItem={({ item }) => <RepositoryItem repository={item}/> }
87 />
88 );
89};
90const RepositoryList = () => {
91 return (
92 <FlatList
93 data={repositories}
94 ItemSeparatorComponent={ItemSeparator}
95 renderItem={({ item: repository }) => <RepositoryItem repository={repository}/> }
96 />
97 );
98 };
99
QUESTION
How to use container exits immediately after startup in docker?
Asked 2020-Oct-29 at 01:31As 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:31Delete 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
ordocker-compose up
it without the-d
option, so it prints its logs to the consoleIf 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
QUESTION
How to append data to existing data object
Asked 2020-Jun-09 at 15:12Im 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(() => axiosInstance.post(`/transition-approval-meta/create/`, action.data))
5yield put({ type: "STORE_WORKFLOW_DATA", 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.
1const initialState = {
2workflowobject:{},
3export function* workerCreateTransitionApproval(action) {
4const data = yield call(() => axiosInstance.post(`/transition-approval-meta/create/`, action.data))
5yield put({ type: "STORE_WORKFLOW_DATA", payload: data.data.data, fetch: 'workflowobject' , label: 'transition_approvals'})
6const loadWorkflowObject = (state, action) => {
7 return updateObject(state, {
8 workflowobject: { ...state.workflowobject, [action.label]: action.payload }
9 })
10}
11
12const storeData = (state, action) => {
13 switch (action.fetch) {
14 case 'workflowobject': return loadWorkflowObject(state, action)
15 }
16}
17
18const reducer = (state = initialState, action) => {
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 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:12you can do this:
1const initialState = {
2workflowobject:{},
3export function* workerCreateTransitionApproval(action) {
4const data = yield call(() => axiosInstance.post(`/transition-approval-meta/create/`, action.data))
5yield put({ type: "STORE_WORKFLOW_DATA", payload: data.data.data, fetch: 'workflowobject' , label: 'transition_approvals'})
6const loadWorkflowObject = (state, action) => {
7 return updateObject(state, {
8 workflowobject: { ...state.workflowobject, [action.label]: action.payload }
9 })
10}
11
12const storeData = (state, action) => {
13 switch (action.fetch) {
14 case 'workflowobject': return loadWorkflowObject(state, action)
15 }
16}
17
18const reducer = (state = initialState, action) => {
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) => {
30 return updateObject(state, {
31 workflowobject: { ...state.workflowobject, [action.label]: state. transition_approvals.concat(action.payload) }
32 })
33}
34
QUESTION
Blazor server side: refresh gui of after api call
Asked 2020-May-26 at 12:45I 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<h1>Dashboard</h1>
6
7@if (sensors is null)
8{
9 <p>Laden...</p>
10}
11else
12{
13 if (sensors.Count == 0)
14 {
15 <p>Keine Sensoren vorhanden!</p>
16 }
17 else
18 {
19 foreach (SensorModel sensor in sensors)
20 {
21 <button class="btn btn-lg @StatusColor(sensor.Status) m-2">@sensor.Name @sensor.Message</button>
22 }
23 }
24}
25
26@code {
27
28 private List<SensorModel> 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<h1>Dashboard</h1>
6
7@if (sensors is null)
8{
9 <p>Laden...</p>
10}
11else
12{
13 if (sensors.Count == 0)
14 {
15 <p>Keine Sensoren vorhanden!</p>
16 }
17 else
18 {
19 foreach (SensorModel sensor in sensors)
20 {
21 <button class="btn btn-lg @StatusColor(sensor.Status) m-2">@sensor.Name @sensor.Message</button>
22 }
23 }
24}
25
26@code {
27
28 private List<SensorModel> 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<IActionResult> UpdateState(int id, int status, string? message)
54 {
55 if (id <= 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 () => { _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<h1>Dashboard</h1>
6
7@if (sensors is null)
8{
9 <p>Laden...</p>
10}
11else
12{
13 if (sensors.Count == 0)
14 {
15 <p>Keine Sensoren vorhanden!</p>
16 }
17 else
18 {
19 foreach (SensorModel sensor in sensors)
20 {
21 <button class="btn btn-lg @StatusColor(sensor.Status) m-2">@sensor.Name @sensor.Message</button>
22 }
23 }
24}
25
26@code {
27
28 private List<SensorModel> 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<IActionResult> UpdateState(int id, int status, string? message)
54 {
55 if (id <= 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 () => { _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:28I 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...
QUESTION
A fast way to fill contours with the ability to export to a polygon-like format
Asked 2020-May-09 at 16:19I have a number of medical pictures presented on canvas, as an example below.
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.
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)?
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:
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).
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.
ANSWER
Answered 2020-May-09 at 16:19Here 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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
21 ctx.strokeStyle = color
22
23 Object.values(points).forEach(ps => {
24 ctx.beginPath()
25 ctx.moveTo(ps[0].x, ps[0].y)
26 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
27 ctx.closePath()
28 ctx.stroke()
29 })
30}
31const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
60 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
61}
62document.querySelector('input[type=range]').onmouseup = e => {
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 => {
68 PARAMS.eps = parseFloat(e.target.value)
69 dumpParams(PARAMS)
70 runCv(PARAMS)
71}
72document.querySelector('#imageSrc').onclick = e => {
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 => {
82 const arr = []
83 for (let j = 0; j < 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) => CVREADY = resolve)
95
96const runCv = async ({ threshold, anchor, eps }) => {
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 < 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) >= 1
117 const area = cv.contourArea(ci)
118 if (inside && area < 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'] = () => {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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
21 ctx.strokeStyle = color
22
23 Object.values(points).forEach(ps => {
24 ctx.beginPath()
25 ctx.moveTo(ps[0].x, ps[0].y)
26 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
27 ctx.closePath()
28 ctx.stroke()
29 })
30}
31const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
60 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
61}
62document.querySelector('input[type=range]').onmouseup = e => {
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 => {
68 PARAMS.eps = parseFloat(e.target.value)
69 dumpParams(PARAMS)
70 runCv(PARAMS)
71}
72document.querySelector('#imageSrc').onclick = e => {
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 => {
82 const arr = []
83 for (let j = 0; j < 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) => CVREADY = resolve)
95
96const runCv = async ({ threshold, anchor, eps }) => {
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 < 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) >= 1
117 const area = cv.contourArea(ci)
118 if (inside && area < 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'] = () => {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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
21 ctx.strokeStyle = color
22
23 Object.values(points).forEach(ps => {
24 ctx.beginPath()
25 ctx.moveTo(ps[0].x, ps[0].y)
26 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
27 ctx.closePath()
28 ctx.stroke()
29 })
30}
31const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
60 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
61}
62document.querySelector('input[type=range]').onmouseup = e => {
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 => {
68 PARAMS.eps = parseFloat(e.target.value)
69 dumpParams(PARAMS)
70 runCv(PARAMS)
71}
72document.querySelector('#imageSrc').onclick = e => {
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 => {
82 const arr = []
83 for (let j = 0; j < 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) => CVREADY = resolve)
95
96const runCv = async ({ threshold, anchor, eps }) => {
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 < 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) >= 1
117 const area = cv.contourArea(ci)
118 if (inside && area < 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'] = () => {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<input type="range" min="0" max="100"/><br/>
156eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
157params: <span id="params"></span><br/>
158<br/>
159<canvas id="imageSrc" height="400" width="400"/></canvas>
160<canvas id="final" height="400" width="400"></canvas>
161<br/>
162<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
163<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
164<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>
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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
21 ctx.strokeStyle = color
22
23 Object.values(points).forEach(ps => {
24 ctx.beginPath()
25 ctx.moveTo(ps[0].x, ps[0].y)
26 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
27 ctx.closePath()
28 ctx.stroke()
29 })
30}
31const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
60 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
61}
62document.querySelector('input[type=range]').onmouseup = e => {
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 => {
68 PARAMS.eps = parseFloat(e.target.value)
69 dumpParams(PARAMS)
70 runCv(PARAMS)
71}
72document.querySelector('#imageSrc').onclick = e => {
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 => {
82 const arr = []
83 for (let j = 0; j < 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) => CVREADY = resolve)
95
96const runCv = async ({ threshold, anchor, eps }) => {
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 < 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) >= 1
117 const area = cv.contourArea(ci)
118 if (inside && area < 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'] = () => {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<input type="range" min="0" max="100"/><br/>
156eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
157params: <span id="params"></span><br/>
158<br/>
159<canvas id="imageSrc" height="400" width="400"/></canvas>
160<canvas id="final" height="400" width="400"></canvas>
161<br/>
162<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
163<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
164<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>// 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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
184 ctx.strokeStyle = color
185
186 Object.values(points).forEach(ps => {
187 ctx.beginPath()
188 ctx.moveTo(ps[0].x, ps[0].y)
189 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
190 ctx.closePath()
191 ctx.stroke()
192 })
193}
194const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
224 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
225}
226document.querySelector('input[type=range]').onmouseup = e => {
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 => {
232 PARAMS.eps = parseFloat(e.target.value)
233 dumpParams(PARAMS)
234 runCv(PARAMS)
235}
236document.querySelector('#imageSrc').onclick = e => {
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 => {
250 if (Date.now() - DEBOUNCE < 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 => {
261 const arr = []
262 for (let j = 0; j < 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 }) => {
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) => CVREADY = resolve)
296
297const runCv = async ({ threshold, anchor, eps, radius }) => {
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 < 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) >= 1
318 const area = cv.contourArea(ci)
319 if (inside && area < 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'] = () => {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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
21 ctx.strokeStyle = color
22
23 Object.values(points).forEach(ps => {
24 ctx.beginPath()
25 ctx.moveTo(ps[0].x, ps[0].y)
26 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
27 ctx.closePath()
28 ctx.stroke()
29 })
30}
31const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
60 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
61}
62document.querySelector('input[type=range]').onmouseup = e => {
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 => {
68 PARAMS.eps = parseFloat(e.target.value)
69 dumpParams(PARAMS)
70 runCv(PARAMS)
71}
72document.querySelector('#imageSrc').onclick = e => {
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 => {
82 const arr = []
83 for (let j = 0; j < 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) => CVREADY = resolve)
95
96const runCv = async ({ threshold, anchor, eps }) => {
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 < 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) >= 1
117 const area = cv.contourArea(ci)
118 if (inside && area < 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'] = () => {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<input type="range" min="0" max="100"/><br/>
156eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
157params: <span id="params"></span><br/>
158<br/>
159<canvas id="imageSrc" height="400" width="400"/></canvas>
160<canvas id="final" height="400" width="400"></canvas>
161<br/>
162<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
163<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
164<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>// 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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
184 ctx.strokeStyle = color
185
186 Object.values(points).forEach(ps => {
187 ctx.beginPath()
188 ctx.moveTo(ps[0].x, ps[0].y)
189 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
190 ctx.closePath()
191 ctx.stroke()
192 })
193}
194const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
224 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
225}
226document.querySelector('input[type=range]').onmouseup = e => {
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 => {
232 PARAMS.eps = parseFloat(e.target.value)
233 dumpParams(PARAMS)
234 runCv(PARAMS)
235}
236document.querySelector('#imageSrc').onclick = e => {
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 => {
250 if (Date.now() - DEBOUNCE < 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 => {
261 const arr = []
262 for (let j = 0; j < 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 }) => {
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) => CVREADY = resolve)
296
297const runCv = async ({ threshold, anchor, eps, radius }) => {
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 < 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) >= 1
318 const area = cv.contourArea(ci)
319 if (inside && area < 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'] = () => {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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
21 ctx.strokeStyle = color
22
23 Object.values(points).forEach(ps => {
24 ctx.beginPath()
25 ctx.moveTo(ps[0].x, ps[0].y)
26 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
27 ctx.closePath()
28 ctx.stroke()
29 })
30}
31const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
60 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
61}
62document.querySelector('input[type=range]').onmouseup = e => {
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 => {
68 PARAMS.eps = parseFloat(e.target.value)
69 dumpParams(PARAMS)
70 runCv(PARAMS)
71}
72document.querySelector('#imageSrc').onclick = e => {
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 => {
82 const arr = []
83 for (let j = 0; j < 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) => CVREADY = resolve)
95
96const runCv = async ({ threshold, anchor, eps }) => {
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 < 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) >= 1
117 const area = cv.contourArea(ci)
118 if (inside && area < 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'] = () => {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<input type="range" min="0" max="100"/><br/>
156eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
157params: <span id="params"></span><br/>
158<br/>
159<canvas id="imageSrc" height="400" width="400"/></canvas>
160<canvas id="final" height="400" width="400"></canvas>
161<br/>
162<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
163<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
164<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>// 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 = () => {
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 && ctx.clearRect(0, 0, 400, 400)
184 ctx.strokeStyle = color
185
186 Object.values(points).forEach(ps => {
187 ctx.beginPath()
188 ctx.moveTo(ps[0].x, ps[0].y)
189 ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
190 ctx.closePath()
191 ctx.stroke()
192 })
193}
194const binarize = (src, threshold) => {
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 => {
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) => {
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 }) => {
224 document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
225}
226document.querySelector('input[type=range]').onmouseup = e => {
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 => {
232 PARAMS.eps = parseFloat(e.target.value)
233 dumpParams(PARAMS)
234 runCv(PARAMS)
235}
236document.querySelector('#imageSrc').onclick = e => {
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 => {
250 if (Date.now() - DEBOUNCE < 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 => {
261 const arr = []
262 for (let j = 0; j < 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 }) => {
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) => CVREADY = resolve)
296
297const runCv = async ({ threshold, anchor, eps, radius }) => {
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 < 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) >= 1
318 const area = cv.contourArea(ci)
319 if (inside && area < 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'] = () => {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<input type="range" min="0" max="100"/><br/>
362eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
363params: <span id="params"></span><br/>
364<br/>
365<canvas id="imageSrc" height="400" width="400"/></canvas>
366<canvas id="final" height="400" width="400"></canvas>
367<br/>
368<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
369<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
370<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>
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