Repository: teleporthq/teleport-code-generators
Pull request #560 (2021-04-21 13:47:11)

(refactor): Extending validations for events in UIDL
I've performed checks by doing these edits to the following files, to fix this [issue](https://github.com/teleporthq/teleport-code-generators/issues/277)

**/packages/teleport-test/src/standalone.ts**
```
...
import componentJSON from '../../../examples/uidl-samples/component.json'
import Validator from '../../teleport-uidl-validator/src/validator'
const componentUIDL = (componentJSON as unknown) as ComponentUIDL;
...
const run = async () => {
 try {
  ...
  let validator = new Validator();
  result = validator.validateComponentContent(componentUIDL);
  console.log(result);
 } catch(e) {
  ...
 }
}
```

/examples/uidl-samples/component.json
```
{
  "name": "Simple Component",
  "node": {
    "type": "element",
    "content": {
      "elementType": "container",
      "children": [{
        "type": "element",
        "content": {
          "elementType": "button",
          "name": "openmodal",
          "children": [{
                  "type": "static",
                  "content": "Show Popup"
                }],
          "events": {
            "click": [{
            "type": "stateChange",
            "modifies": "isOpen",
            "newState": true
          }]
          },
          "attrs": { 
            "data-static-attr": {
              "type": "static",
              "content": "test"
            }
          },
          "attributes":{ 
            "htmlFor": {
             "type": "static",
              "content": "test"
            }
          }
        }
      }]
    }
  }
}
```



Patch:
diff --git a/packages/teleport-plugin-angular-base-component/src/utils.ts b/packages/teleport-plugin-angular-base-component/src/utils.ts
index 3ca35554..2c37eca3 100644
--- a/packages/teleport-plugin-angular-base-component/src/utils.ts
+++ b/packages/teleport-plugin-angular-base-component/src/utils.ts
@@ -8,6 +8,7 @@ import {
   UIDLPropDefinition,
   UIDLStateDefinition,
   UIDLEventHandlerStatement,
+  UIDLPropCallEvent,
 } from '@teleporthq/teleport-types'
 
 export const generateExportAST = (
@@ -42,6 +43,7 @@ export const generateExportAST = (
         [t.decorator(t.callExpression(t.identifier('Output'), []))]
       )
     }
+
     return t.classProperty(
       t.identifier(propKey),
       ASTUtils.convertValueToLiteral(propDefinitions[propKey].defaultValue),
@@ -167,7 +169,7 @@ const createMethodsObject = (
 }
 
 export const createPropCallStatement = (
-  eventHandlerStatement: UIDLEventHandlerStatement,
+  eventHandlerStatement: UIDLPropCallEvent,
   propDefinitions: Record<string, UIDLPropDefinition>,
   t = types
 ) => {
diff --git a/packages/teleport-plugin-common/src/builders/ast-builders.ts b/packages/teleport-plugin-common/src/builders/ast-builders.ts
index d8956d93..9e7ac17c 100644
--- a/packages/teleport-plugin-common/src/builders/ast-builders.ts
+++ b/packages/teleport-plugin-common/src/builders/ast-builders.ts
@@ -7,9 +7,9 @@ import {
 } from '../utils/ast-utils'
 import {
   ImportIdentifier,
-  UIDLEventHandlerStatement,
   EntryFileOptions,
   UIDLGlobalAsset,
+  UIDLStateModifierEvent,
 } from '@teleporthq/teleport-types'
 import { UIDLUtils } from '@teleporthq/teleport-shared'
 
@@ -164,7 +164,7 @@ export const createComponentDecorator = (params: Record<string, unknown>, t = ty
   )
 }
 
-export const createStateChangeStatement = (statement: UIDLEventHandlerStatement, t = types) => {
+export const createStateChangeStatement = (statement: UIDLStateModifierEvent, t = types) => {
   const { modifies, newState } = statement
 
   const rightOperand =
diff --git a/packages/teleport-plugin-common/src/node-handlers/node-to-html/utils.ts b/packages/teleport-plugin-common/src/node-handlers/node-to-html/utils.ts
index 2e588536..40aebd0b 100644
--- a/packages/teleport-plugin-common/src/node-handlers/node-to-html/utils.ts
+++ b/packages/teleport-plugin-common/src/node-handlers/node-to-html/utils.ts
@@ -70,12 +70,13 @@ export const handleEvent = (
 
   if (eventHandlerStatements.length === 1) {
     const statement = eventHandlerStatements[0]
-    const isPropEvent = statement && statement.type === 'propCall' && statement.calls
 
-    if (isPropEvent) {
+    if (statement.type === 'propCall' && statement.calls) {
       const eventEmitter = templateSyntax.eventEmmitter(statement.calls)
       hastUtils.addAttributeToNode(htmlNode, eventHandlerKey, eventEmitter)
-    } else {
+    }
+
+    if (statement.type === 'stateChange') {
       hastUtils.addAttributeToNode(
         htmlNode,
         eventHandlerKey,
diff --git a/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts b/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts
index 1b233c36..4c71217d 100644
--- a/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts
+++ b/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts
@@ -9,6 +9,8 @@ import {
   UIDLStateDefinition,
   UIDLEventHandlerStatement,
   UIDLConditionalExpression,
+  UIDLPropCallEvent,
+  UIDLStateModifierEvent,
 } from '@teleporthq/teleport-types'
 
 import {
@@ -67,7 +69,7 @@ export const addEventHandlerToTag = (
 }
 
 const createPropCallStatement = (
-  eventHandlerStatement: UIDLEventHandlerStatement,
+  eventHandlerStatement: UIDLPropCallEvent,
   propDefinitions: Record<string, UIDLPropDefinition>,
   options: JSXGenerationOptions,
   t = types
@@ -97,7 +99,7 @@ const createPropCallStatement = (
 }
 
 const createStateChangeStatement = (
-  eventHandlerStatement: UIDLEventHandlerStatement,
+  eventHandlerStatement: UIDLStateModifierEvent,
   stateDefinitions: Record<string, UIDLStateDefinition>,
   options: JSXGenerationOptions,
   t = types
diff --git a/packages/teleport-plugin-vue-base-component/src/utils.ts b/packages/teleport-plugin-vue-base-component/src/utils.ts
index 997d094e..0ec1e9fa 100644
--- a/packages/teleport-plugin-vue-base-component/src/utils.ts
+++ b/packages/teleport-plugin-vue-base-component/src/utils.ts
@@ -7,6 +7,7 @@ import {
   UIDLStateDefinition,
   UIDLEventHandlerStatement,
   ComponentUIDL,
+  UIDLPropCallEvent,
 } from '@teleporthq/teleport-types'
 
 export const extractStateObject = (stateDefinitions: Record<string, UIDLStateDefinition>) => {
@@ -160,7 +161,7 @@ const createMethodsObject = (
 }
 
 export const createPropCallStatement = (
-  eventHandlerStatement: UIDLEventHandlerStatement,
+  eventHandlerStatement: UIDLPropCallEvent,
   propDefinitions: Record<string, UIDLPropDefinition>,
   t = types
 ) => {
diff --git a/packages/teleport-types/src/uidl.ts b/packages/teleport-types/src/uidl.ts
index 3b2c39c3..3fe7c917 100755
--- a/packages/teleport-types/src/uidl.ts
+++ b/packages/teleport-types/src/uidl.ts
@@ -244,14 +244,20 @@ export type UIDLLinkNode =
   | UIDLMailLinkNode
   | UIDLPhoneLinkNode
 
-export interface UIDLEventHandlerStatement {
-  type: string
-  modifies?: string
-  newState?: string | number | boolean
-  calls?: string
+export interface UIDLPropCallEvent {
+  type: 'propCall'
+  calls: string
   args?: Array<string | number | boolean>
 }
 
+export interface UIDLStateModifierEvent {
+  type: 'stateChange'
+  modifies: string
+  newState: string | number | boolean
+}
+
+export type UIDLEventHandlerStatement = UIDLPropCallEvent | UIDLStateModifierEvent
+
 export type UIDLDependency = UIDLLocalDependency | UIDLExternalDependency
 
 export interface UIDLPeerDependency {
@@ -343,7 +349,7 @@ export interface UIDLStyleStateCondition {
   content: UIDLElementStyleStates
 }
 
-export type UIDLElementStyleStates = 'hover' | 'active' | 'focus' | 'disabled'
+export type UIDLElementStyleStates = 'hover' | 'active' | 'focus' | 'focus-within' | 'focus-visible' |  'disabled' | 'visited' | 'checked' | 'placeholder'
 
 export interface UIDLStyleSetDefinition {
   id: string
diff --git a/packages/teleport-uidl-validator/src/decoders/utils.ts b/packages/teleport-uidl-validator/src/decoders/utils.ts
index 0ba32f0e..20116601 100644
--- a/packages/teleport-uidl-validator/src/decoders/utils.ts
+++ b/packages/teleport-uidl-validator/src/decoders/utils.ts
@@ -10,6 +10,7 @@ import {
   boolean,
   array,
   lazy,
+  oneOf
 } from '@mojotech/json-type-validation'
 import {
   UIDLStaticValue,
@@ -56,6 +57,8 @@ import {
   VUIDLStyleSetMediaCondition,
   VUIDLStyleSetStateCondition,
   VUIDLDesignTokens,
+  UIDLPropCallEvent,
+  UIDLStateModifierEvent,
 } from '@teleporthq/teleport-types'
 import { CustomCombinators } from './custom-combinators'
 
@@ -284,14 +287,23 @@ export const styleValueDecoder: Decoder<UIDLStyleValue> = union(
 
 export const styleDefinitionsDecoder: Decoder<UIDLStyleDefinitions> = dict(styleValueDecoder)
 
-export const eventHandlerStatementDecoder: Decoder<UIDLEventHandlerStatement> = object({
-  type: string(),
-  calls: optional(string()),
-  modifies: optional(string()),
-  newState: optional(union(string(), number(), boolean())),
+export const eventHandlerStatementDecoder: Decoder<UIDLEventHandlerStatement> = union(
+  lazy(() => propCallEventDecoder),
+  lazy(() => stateChangeEventDecoder)
+)
+
+export const propCallEventDecoder: Decoder<UIDLPropCallEvent> = object({
+  type: constant('propCall'),
+  calls: string(),
   args: optional(array(union(string(), number(), boolean()))),
 })
 
+export const stateChangeEventDecoder: Decoder<UIDLStateModifierEvent> = object({
+  type: constant('stateChange'),
+  modifies: string(),
+  newState: union(string(), number(), boolean()),
+})
+
 export const urlLinkNodeDecoder: Decoder<VUIDLURLLinkNode> = object({
   type: constant('url'),
   content: object({
@@ -341,11 +353,16 @@ export const rawValueDecoder: Decoder<UIDLRawValue> = object({
   content: string(),
 })
 
-export const elementStateDecoder: Decoder<UIDLElementStyleStates> = union(
+export const elementStateDecoder: Decoder<UIDLElementStyleStates> = oneOf(
   constant('hover'),
   constant('active'),
   constant('focus'),
-  constant('disabled')
+  constant('focus-within'),
+  constant('focus-visible'),
+  constant('disabled'),
+  constant('visited'),
+  constant('checked'),
+  constant('placeholder')
 )
 
 export const elementStyleWithStateConditionDecoder: Decoder<UIDLStyleStateCondition> = object({
diff --git a/packages/teleport-uidl-validator/src/validator/utils.ts b/packages/teleport-uidl-validator/src/validator/utils.ts
index 23534eb3..b9811efb 100644
--- a/packages/teleport-uidl-validator/src/validator/utils.ts
+++ b/packages/teleport-uidl-validator/src/validator/utils.ts
@@ -99,6 +99,25 @@ export const checkDynamicDefinitions = (input: Record<string, unknown>) => {
   const errors: string[] = []
 
   UIDLUtils.traverseNodes(input.node as UIDLNode, (node) => {
+    if (node.type === 'element' && node.content.events) {
+      Object.keys(node.content.events).forEach((eventKey) => {
+        node.content.events[eventKey].forEach((event) => {
+          if (event.type === 'stateChange' && !stateKeys.includes(event.modifies)) {
+            const errorMsg = `"${event.modifies}" is used in events, but not defined. Please add it in stateDefinitions`
+            errors.push(errorMsg)
+            return
+          }
+
+          if (event.type === 'propCall' && !propKeys.includes(event.calls)) {
+            errors.push(
+              `"${event.calls}" is used in events, but missing from propDefinitons. Please add it in propDefinitions `
+            )
+            return
+          }
+        })
+      })
+    }
+
     if (node.type === 'dynamic' && node.content.referenceType === 'prop') {
       if (!dynamicPathExistsInDefinitions(node.content.id, propKeys)) {
         const errorMsg = `"${node.content.id}" is used but not defined. Please add it in propDefinitions`


Test patch:
diff --git a/packages/teleport-component-generator-angular/__tests__/end2end/index.ts b/packages/teleport-component-generator-angular/__tests__/end2end/index.ts
index c4894977..cb5f8d37 100644
--- a/packages/teleport-component-generator-angular/__tests__/end2end/index.ts
+++ b/packages/teleport-component-generator-angular/__tests__/end2end/index.ts
@@ -9,6 +9,7 @@ import {
   GeneratedFile,
   UIDLEventDefinitions,
   UIDLPropDefinition,
+  UIDLStateDefinition,
 } from '@teleporthq/teleport-types'
 import {
   component,
@@ -90,6 +91,12 @@ describe('Should add EventEmitter and Emit events when a fun is sent via prop',
       defaultValue: '() => {}',
     },
   }
+  const stateDefinitions: Record<string, UIDLStateDefinition> = {
+    fakeState: {
+      type: 'boolean',
+      defaultValue: true,
+    },
+  }
   const events: UIDLEventDefinitions = {
     click: [
       {
@@ -99,7 +106,7 @@ describe('Should add EventEmitter and Emit events when a fun is sent via prop',
       {
         type: 'stateChange',
         modifies: 'fakeState',
-        newState: false,
+        newState: '$toggle',
       },
     ],
   }
@@ -109,7 +116,8 @@ describe('Should add EventEmitter and Emit events when a fun is sent via prop',
       dynamicNode('prop', 'message'),
       elementNode('button', {}, [staticNode('close')], null, null, events),
     ]),
-    propDefinitions
+    propDefinitions,
+    stateDefinitions
   )
 
   it('Adds EmitEmitter to the import', async () => {
@@ -121,6 +129,7 @@ describe('Should add EventEmitter and Emit events when a fun is sent via prop',
     expect(tsFile.content).toContain(`Output, EventEmitter`)
     expect(tsFile.content).toContain(`@Output`)
     expect(tsFile.content).toContain(`onClose: EventEmitter<any> = new EventEmitter()`)
+    expect(tsFile.content).toContain(`this.fakeState = !this.fakeState`)
     expect(tsFile.content).toContain(`this.onClose.emit()`)
     expect(htmlFile.content).toContain(`(click)="handleButtonClick()"`)
   })
diff --git a/packages/teleport-component-generator-vue/__tests__/end2end/index.ts b/packages/teleport-component-generator-vue/__tests__/end2end/index.ts
index 40228322..3478de5f 100644
--- a/packages/teleport-component-generator-vue/__tests__/end2end/index.ts
+++ b/packages/teleport-component-generator-vue/__tests__/end2end/index.ts
@@ -9,6 +9,7 @@ import {
   GeneratedFile,
   UIDLPropDefinition,
   UIDLEventDefinitions,
+  UIDLStateDefinition,
 } from '@teleporthq/teleport-types'
 import { component, elementNode, dynamicNode, staticNode } from '@teleporthq/teleport-uidl-builders'
 
@@ -92,6 +93,12 @@ describe('Should add EventEmitter and Emit events when a fun is sent via prop',
       defaultValue: '() => {}',
     },
   }
+  const stateDefinitions: Record<string, UIDLStateDefinition> = {
+    fakeState: {
+      type: 'boolean',
+      defaultValue: true,
+    },
+  }
   const events: UIDLEventDefinitions = {
     click: [
       {
@@ -111,7 +118,8 @@ describe('Should add EventEmitter and Emit events when a fun is sent via prop',
       dynamicNode('prop', 'message'),
       elementNode('button', {}, [staticNode('close')], null, null, events),
     ]),
-    propDefinitions
+    propDefinitions,
+    stateDefinitions
   )
 
   it('Adds EmitEmitter to the import', async () => {
diff --git a/packages/teleport-uidl-validator/__tests__/validator/component-uidl-with-event-modifier-undefined.json b/packages/teleport-uidl-validator/__tests__/validator/component-uidl-with-event-modifier-undefined.json
new file mode 100644
index 00000000..11478b7e
--- /dev/null
+++ b/packages/teleport-uidl-validator/__tests__/validator/component-uidl-with-event-modifier-undefined.json
@@ -0,0 +1,45 @@
+{
+  "name": "Simple Component",
+  "node": {
+    "type": "element",
+    "content": {
+      "elementType": "container",
+      "children": [
+        {
+          "type": "element",
+          "content": {
+            "elementType": "button",
+            "name": "openmodal",
+            "children": [
+              {
+                "type": "static",
+                "content": "Show Popup"
+              }
+            ],
+            "events": {
+              "click": [
+                {
+                  "type": "stateChange",
+                  "modifies": "isOpen",
+                  "newState": true
+                }
+              ]
+            },
+            "attrs": {
+              "data-static-attr": {
+                "type": "static",
+                "content": "test"
+              }
+            },
+            "attributes": {
+              "htmlFor": {
+                "type": "static",
+                "content": "test"
+              }
+            }
+          }
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/packages/teleport-uidl-validator/__tests__/validator/index.ts b/packages/teleport-uidl-validator/__tests__/validator/index.ts
index a3988ed9..5c4e2b26 100644
--- a/packages/teleport-uidl-validator/__tests__/validator/index.ts
+++ b/packages/teleport-uidl-validator/__tests__/validator/index.ts
@@ -1,5 +1,7 @@
 import { Validator } from '../../src'
 
+// @ts-ignore
+import componentUidlWithEventModifierUndefined from './component-uidl-with-event-modifier-undefined.json'
 // @ts-ignore
 import invalidComponentUidlSample from './component-invalid-sample.json'
 // @ts-ignore
@@ -111,6 +113,19 @@ describe('Validate UIDL', () => {
       expect(validationResult.errorMsg).toEqual('')
       expect(warn).toHaveBeenCalled()
     })
+
+    it('throws an error if event is modifying the state, which is not defined in stateDefinitions', () => {
+      const validator = new Validator()
+      // @ts-ignore
+      expect(() =>
+        validator.validateComponentContent(componentUidlWithEventModifierUndefined)
+      ).toThrow(Error)
+
+      // expect(validationResult.errorMsg).toBe(
+      //   `\nUIDL Component Content Validation Error. Please check the following:
+      //    "isOpen" is used in events, but not defined. Please add it in stateDefinitions
+      // )
+    })
   })
 
   describe('Project UIDL Format', () => {