sunmao-ui icon indicating copy to clipboard operation
sunmao-ui copied to clipboard

Proposal: Define a method to be an async function

Open MrWindlike opened this issue 2 years ago • 3 comments

The Problem

Currently, if we want to call the method after the fetch trait is complete or error, we need to add all the handlers into the fetch trait’s onComplete and use the disabled to control whether they should be executed. In addition, we also need to add the global states to mark the different trigger ways and clear their values after using it which brings a lot of mental burdens.

For example, I want to implement this demand:

  • when click button1: call api → set state value →set input1's value to button1 after complete
  • when click button2: call api → set state value → set input1's value to button2 after complete

The whole application schema to be this:

{
  "version": "sunmao/v1",
  "kind": "Application",
  "metadata": {
    "name": "some App"
  },
  "spec": {
    "components": [
      {
        "id": "api",
        "type": "core/v1/dummy",
        "properties": {},
        "traits": [
          {
            "type": "core/v1/fetch",
            "properties": {
              "url": "",
              "method": "get",
              "lazy": true,
              "disabled": false,
              "headers": {},
              "body": "{{[]}}",
              "bodyType": "json",
              "onComplete": [
                {
                  "componentId": "state",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{api.fetch.data}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "componentId": "input1",
                  "method": {
                    "name": "setInputValue",
                    "parameters": {
                      "value": "button1"
                    }
                  },
                  "disabled": "{{!isClickButton1.value}}",
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "componentId": "input1",
                  "method": {
                    "name": "setInputValue",
                    "parameters": {
                      "value": "button2"
                    }
                  },
                  "disabled": "{{!isClickButton2.value}}",
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "componentId": "isClickButton1",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{false}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "componentId": "isClickButton2",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{false}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ],
              "onError": [
                {
                  "componentId": "isClickButton1",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{false}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "componentId": "isClickButton2",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{false}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ]
            }
          }
        ]
      },
      {
        "id": "isClickButton1",
        "type": "core/v1/dummy",
        "properties": {},
        "traits": [
          {
            "type": "core/v1/state",
            "properties": {
              "key": "value",
              "initialValue": "{{false}}"
            }
          }
        ]
      },
      {
        "id": "isClickButton2",
        "type": "core/v1/dummy",
        "properties": {},
        "traits": [
          {
            "type": "core/v1/state",
            "properties": {
              "key": "value",
              "initialValue": "{{false}}"
            }
          }
        ]
      },
      {
        "id": "input1",
        "type": "chakra_ui/v1/input",
        "properties": {
          "variant": "outline",
          "placeholder": "Please input value",
          "size": "md",
          "focusBorderColor": "",
          "isDisabled": false,
          "isRequired": false,
          "defaultValue": ""
        },
        "traits": []
      },
      {
        "id": "button1",
        "type": "arco/v1/button",
        "properties": {
          "type": "default",
          "status": "default",
          "long": false,
          "size": "default",
          "disabled": false,
          "loading": false,
          "shape": "square",
          "text": "button1"
        },
        "traits": [
          {
            "type": "core/v1/event",
            "properties": {
              "handlers": [
                {
                  "type": "onClick",
                  "componentId": "isClickButton1",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{true}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "type": "onClick",
                  "componentId": "api",
                  "method": {
                    "name": "triggerFetch",
                    "parameters": {}
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ]
            }
          }
        ]
      },
      {
        "id": "button2",
        "type": "arco/v1/button",
        "properties": {
          "type": "default",
          "status": "default",
          "long": false,
          "size": "default",
          "disabled": false,
          "loading": false,
          "shape": "square",
          "text": "button2"
        },
        "traits": [
          {
            "type": "core/v1/event",
            "properties": {
              "handlers": [
                {
                  "type": "onClick",
                  "componentId": "isClickButton2",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{true}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                },
                {
                  "type": "onClick",
                  "componentId": "api",
                  "method": {
                    "name": "triggerFetch",
                    "parameters": {}
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ]
            }
          }
        ]
      },
      {
        "id": "state",
        "type": "core/v1/dummy",
        "properties": {},
        "traits": [
          {
            "type": "core/v1/state",
            "properties": {
              "key": "value"
            }
          }
        ]
      }
    ]
  }
}

The logic turn to JavaScript code just like this:

function trigger () {
  api().then(()=> {
    setState()
    if (isClickButton1) {
      setInput1('button1')
    }
    if (isClickButton2) {
      setInput1('button2')
    }
    setIsClickButton1(false)
    setIsClickButton2(false)
  }).catch(()=> {
    setIsClickButton1(false)
    setIsClickButton2(false)
  })
}

function handleClickButton1 () {
  setIsClickButton1(true)
  trigger()
}

function handleClickButton2 () {
  setIsClickButton2(true)
  trigger()
}

🙃  There are so awful to do this.

Improvement

To improve this, I want to define whether the method is the async function.

The spec to define a method to be the async function may be like this:

{
  methods: [
    {
      name: 'triggerFetch',
      parameters: Type.Object({}),
      async: true
    },
  ],
}

Then if the method is an async function, it would support the onComplete and onError in the schema to call other methods.

// packages/runtime/src/components/_internal/ImplWrapper/hooks/useGlobalHandlerMap.ts
const handler = async (schema) => {
  const method = handlerMap[schema.name];

  if (method.async) {
    try {
      await method(schema.parameters);
      c.onComplete.forEach(onCompleteschema=> generateCallback(onCompleteschema)());
    } catch {
      c.onError.forEach(onErrorSchema=> generateCallback(onErrorSchema)());
    }
  } else {
    method(s.parameters);
  }
};
// packages/runtime/src/traits/core/Fetch.tsx
export default implementRuntimeTrait({
  version: CORE_VERSION,
  metadata: {
    name: CoreTraitName.Fetch,
    description: 'fetch data to store',
  },
  ...
})(()=> {
  ...

  const fetchData = () => {
    ...
    return fetch();
  };

  subscribeMethods({
      triggerFetch() {
        return fetchData();
      },
      ...
  });

  ...
})

The whole schema may be like this:

{
  "version": "sunmao/v1",
  "kind": "Application",
  "metadata": {
    "name": "some App"
  },
  "spec": {
    "components": [
      {
        "id": "api",
        "type": "core/v1/dummy",
        "properties": {},
        "traits": [
          {
            "type": "core/v1/fetch",
            "properties": {
              "url": "",
              "method": "get",
              "lazy": true,
              "disabled": false,
              "headers": {},
              "body": "{{[]}}",
              "bodyType": "json",
              "onComplete": [
                {
                  "componentId": "state",
                  "method": {
                    "name": "setValue",
                    "parameters": {
                      "key": "value",
                      "value": "{{api.fetch.data}}"
                    }
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ],
              "onError": []
            }
          }
        ]
      },
      {
        "id": "input1",
        "type": "chakra_ui/v1/input",
        "properties": {
          "variant": "outline",
          "placeholder": "Please input value",
          "size": "md",
          "focusBorderColor": "",
          "isDisabled": false,
          "isRequired": false,
          "defaultValue": ""
        },
        "traits": []
      },
      {
        "id": "button1",
        "type": "arco/v1/button",
        "properties": {
          "type": "default",
          "status": "default",
          "long": false,
          "size": "default",
          "disabled": false,
          "loading": false,
          "shape": "square",
          "text": "button1"
        },
        "traits": [
          {
            "type": "core/v1/event",
            "properties": {
              "handlers": [
                {
                  "type": "onClick",
                  "componentId": "api",
                  "method": {
                    "name": "triggerFetch",
                    "parameters": {},
+                   "onComplete": [
+                     {
+                       "componentId": "input1",
+                       "method": {
+                         "name": "setInputValue",
+                         "parameters": {
+                           "key": "value",
+                           "value": "button1"
+                         }
+                       },
+                       "disabled": false,
+                       "wait": {
+                         "type": "delay",
+                         "time": 0
+                       }
+                     }
+                   ]
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ]
            }
          }
        ]
      },
      {
        "id": "button2",
        "type": "arco/v1/button",
        "properties": {
          "type": "default",
          "status": "default",
          "long": false,
          "size": "default",
          "disabled": false,
          "loading": false,
          "shape": "square",
          "text": "button2"
        },
        "traits": [
          {
            "type": "core/v1/event",
            "properties": {
              "handlers": [
                {
                  "type": "onClick",
                  "componentId": "api",
                  "method": {
                    "name": "triggerFetch",
                    "parameters": {},
+                   "onComplete": [
+                     {
+                       "componentId": "input1",
+                       "method": {
+                         "name": "setInputValue",
+                         "parameters": {
+                           "key": "value",
+                           "value": "button2"
+                         }
+                       },
+                       "disabled": false,
+                       "wait": {
+                         "type": "delay",
+                         "time": 0
+                       }
+                     }
+                   ]
                  },
                  "disabled": false,
                  "wait": {
                    "type": "delay",
                    "time": 0
                  }
                }
              ]
            }
          }
        ]
      },
      {
        "id": "state",
        "type": "core/v1/dummy",
        "properties": {},
        "traits": [
          {
            "type": "core/v1/state",
            "properties": {
              "key": "value"
            }
          }
        ]
      }
    ]
  }
}

The logic turn to JavaScript code just like this:

async function trigger () {
  await api()
  setState()
}

function handleClickButton1 () {
  trigger().then(()=> setInput1('button1'))
}

function handleClickButton2 () {
  trigger().then(()=> setInput1('button2'))
}

Compare that to the before schema:

  • Remove the state isClickButton1, isClickButton2
  • Move the onComplete from the fetch trait to the button1 and button2

MrWindlike avatar May 31 '22 10:05 MrWindlike

function trigger () {
  api().then(()=> {
    setState()
    if (isClickButton1) {
      setInput1('button1')
    }
    if (isClickButton2) {
      setInput1('button2')
    }
    setIsClickButton1(false)
    setIsClickButton2(false)
  }).catch(()=> {
    setIsClickButton1(false)
    setIsClickButton2(false)
  })
}

function handleClickButton1 () {
  setIsClickButton1(true)
  trigger()
}

function handleClickButton2 () {
  setIsClickButton2(true)
  trigger()
}

Why do these two buttons share the same trigger, instead of

function trigger1 () {
  api().then(()=> {
    setInput1('button1')
  })
}

function trigger2 () {
  api().then(()=> {
    setInput2('button2')
  })
}

function handleClickButton1 () {
  trigger1()
}

function handleClickButton2 () {
  trigger2()
}

Yuyz0112 avatar May 31 '22 12:05 Yuyz0112

Why do these two buttons share the same trigger

Because they use the same fetch trait to call the same API, they use the same trigger. However, the things the buttons need to do in onComplete is different.

Do you mean to use the different fetch traits to call the same API and then they can use the different onComplete to solve the problem?

MrWindlike avatar May 31 '22 15:05 MrWindlike

They can't reuse the parameters and some common logic and so on if use the different fetch traits to call the same API. I think this isn't the best way to solve the problem.

MrWindlike avatar May 31 '22 15:05 MrWindlike