diff --git a/component_specs/BUILD b/component_specs/BUILD index 91014581b..a3ce14834 100644 --- a/component_specs/BUILD +++ b/component_specs/BUILD @@ -1,4 +1,4 @@ -load("//build_defs:defaults.bzl", "py_binary", "py_library") +load("//build_defs:defaults.bzl", "THIRD_PARTY_PY_ABSL_PY", "py_binary", "py_library") package( default_visibility = ["//build_defs:mesop_internal"], @@ -19,5 +19,5 @@ py_binary( deps = [ # ":component_specs", "//mesop/protos:ui_py_pb2", - ], + ] + THIRD_PARTY_PY_ABSL_PY, ) diff --git a/component_specs/checkbox_spec.py b/component_specs/checkbox_spec.py index ad23c3d03..4bbdf653b 100644 --- a/component_specs/checkbox_spec.py +++ b/component_specs/checkbox_spec.py @@ -1,3 +1,7 @@ +import os.path + +from absl import app, flags + import mesop.protos.ui_pb2 as pb from component_specs.generate_from_spec import ( generate_ng_template, @@ -6,22 +10,32 @@ generate_py_component, ) +FLAGS = flags.FLAGS + +flags.DEFINE_bool("write", False, "set to true to write files.") + + # // # // {{config().getLabel()}} # // CHECKBOX_SPEC = pb.ComponentSpec( type_name="checkbox", element_name="mat-checkbox", + ng_module=pb.NgModuleSpec( + import_path="@angular/material/checkbox", + module_name="MatCheckboxModule", + other_symbols=["MatCheckboxChange"], + ), props=[ pb.ElementProp( key="checked", - property_binding=pb.PropertyBinding(name="value", type=pb.JsType.STRING), + property_binding=pb.PropertyBinding(name="value", type=pb.JsType.BOOL), ), # TODO: need a special marker for value (default value) pb.ElementProp( key="change", event_binding=pb.EventBinding( event_name="MatCheckboxChange", - props=[pb.EventProp(key="changed", type=pb.JsType.BOOL)], + props=[pb.EventProp(key="checked", type=pb.JsType.BOOL)], ), ), ], @@ -29,12 +43,59 @@ ) +def main(argv): + if FLAGS.write: + write( + generate_ng_template(CHECKBOX_SPEC), + CHECKBOX_SPEC.type_name, + ext="ng.html", + ) + write( + generate_ng_ts(CHECKBOX_SPEC), + CHECKBOX_SPEC.type_name, + ext="ts", + ) + write( + generate_proto_schema(CHECKBOX_SPEC), + CHECKBOX_SPEC.type_name, + ext="proto", + ) + write( + generate_py_component(CHECKBOX_SPEC), + CHECKBOX_SPEC.type_name, + ext="py", + ) + + print("Written files") + else: + print(".ng.html:") + print(generate_ng_template(CHECKBOX_SPEC)) + print(".ts:") + print(generate_ng_ts(CHECKBOX_SPEC)) + print(".proto:") + print(generate_proto_schema(CHECKBOX_SPEC)) + print(":.py:") + print(generate_py_component(CHECKBOX_SPEC)) + + +def write(contents: str, name: str, ext: str): + with open( + os.path.join( + get_bazel_workspace_directory(), f"mesop/components/{name}/{name}.{ext}" + ), + "w", + ) as f: + f.write(contents) + + +def get_bazel_workspace_directory() -> str: + value = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + assert value + return value + + +# workspace_dir = get_bazel_workspace_directory() +# print(f"Bazel workspace directory: {workspace_dir}") + if __name__ == "__main__": - print(".ng.html:") - print(generate_ng_template(CHECKBOX_SPEC)) - print(".ts:") - print(generate_ng_ts(CHECKBOX_SPEC)) - print(".proto:") - print(generate_proto_schema(CHECKBOX_SPEC)) - print(":.py:") - print(generate_py_component(CHECKBOX_SPEC)) + app.run(main) diff --git a/component_specs/fixtures/component_name.ts b/component_specs/fixtures/component_name.ts index a27c90481..b4247ae7c 100644 --- a/component_specs/fixtures/component_name.ts +++ b/component_specs/fixtures/component_name.ts @@ -1,5 +1,6 @@ import {Component, Input} from '@angular/core'; import { + UserEvent, Key, Type, } from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; @@ -7,15 +8,15 @@ import {ComponentNameType} from 'mesop/mesop/components/component_name/component import {Channel} from '../../web/src/services/channel'; @Component({ - // selector: 'mesop-{component-name}', templateUrl: 'component_name.ng.html', standalone: true, + // GENERATE_NG_IMPORTS: }) export class ComponentNameComponent { @Input({required: true}) type!: Type; @Input() key!: Key; private _config!: ComponentNameType; - value: any; + value!: any; constructor(private readonly channel: Channel) {} @@ -23,7 +24,7 @@ export class ComponentNameComponent { this._config = ComponentNameType.deserializeBinary( this.type.getValue() as unknown as Uint8Array, ); - this.value = this._config.getDefaultValue(); + this.value = this._config.getValue(); } config(): ComponentNameType { diff --git a/component_specs/generate_from_spec.py b/component_specs/generate_from_spec.py index aa49abfdb..a3e66b3a2 100644 --- a/component_specs/generate_from_spec.py +++ b/component_specs/generate_from_spec.py @@ -1,13 +1,19 @@ +from typing import Any + import mesop.protos.ui_pb2 as pb """ -Spec recipe: -1. Generate Angular template (done) -2. Generate Angular TS (done) -3. Generate proto (done) -4. Generate Python method (done) +Generates from component spec the following: +1. Angular template (ng.html) +2. Angular TS +3. Proto schema (.proto) +4. Python component TODOs: +- seperate out component spec protos into its own file +- make it clear what the spelling (camel vs. snake_case) + +MAYBEs: - doesn't handle event with multiple properties - have better naming for events (either explicit control, or implicitly strip out Mat) """ @@ -40,16 +46,24 @@ def generate_ng_ts(spec: pb.ComponentSpec) -> str: default_value_prop = [ prop for prop in spec.props if prop.property_binding.name == "value" ][0] + symbols = ",".join( + [spec.ng_module.module_name] + list(spec.ng_module.other_symbols) + ) + symbols = "{" + symbols + "}" + ts_template = ( + f"import {symbols} from '{spec.ng_module.import_path}'\n" + ts_template + ) # Do simple string replacements ts_template = ( ts_template.replace("component_name", spec.type_name) .replace("ComponentName", upper_camel_case(spec.type_name)) .replace( - "value: any;", - f"value: {format_js_type(default_value_prop.property_binding.type)};", + "value!: any;", + f"value!: {format_js_type(default_value_prop.property_binding.type)};", ) .replace("// INSERT_EVENT_METHODS:", generate_ts_event_methods(spec)) + .replace("// GENERATE_NG_IMPORTS:", generate_ng_imports(spec)) ) # Build event handlers @@ -57,12 +71,16 @@ def generate_ng_ts(spec: pb.ComponentSpec) -> str: if prop.HasField("event_binding"): ts_template = ts_template.replace( f"on{prop.event_binding.event_name}", - f"handle{prop.event_binding.event_name}", + f"on{prop.event_binding.event_name}", ) return ts_template +def generate_ng_imports(spec: pb.ComponentSpec) -> str: + return f"imports: [{spec.ng_module.module_name}]" + + def generate_proto_schema(spec: pb.ComponentSpec) -> str: fields: list[str] = [] index = 0 @@ -70,14 +88,18 @@ def generate_proto_schema(spec: pb.ComponentSpec) -> str: for prop in spec.props: index += 1 if prop.HasField("property_binding"): - fields.append(f"{prop.property_binding.name} = {index};") + fields.append( + f"{format_js_type_for_proto(prop.property_binding.type)} {prop.property_binding.name} = {index};" + ) if prop.HasField("event_binding"): fields.append( - f"on_{snake_case(prop.event_binding.event_name)}_handler_id = {index};" + f"string on_{snake_case(prop.event_binding.event_name)}_handler_id = {index};" ) if spec.HasField("content"): index += 1 - fields.append(f"{spec.content.name} = {index};") + fields.append( + f"{format_js_type_for_proto(spec.content.type)} {spec.content.name} = {index};" + ) message_contents = ( "{\n" + "\n".join([" " + field for field in fields]) + "\n}" @@ -133,26 +155,26 @@ class {event_binding.event_name}Event(MesopEvent): {event_binding.event_name}Event, lambda event, key: {event_binding.event_name}Event( key=key, - {event_binding.props[0].key}=event.{format_js_type_for_python(event_binding.props[0].type)}, + {event_binding.props[0].key}=event.{format_js_type_for_proto(event_binding.props[0].type)}, ), ) """ def generate_py_component_params(spec: pb.ComponentSpec) -> str: - out: list[str] = [] + out: list[str] = ["key: str | None = None"] for prop in spec.props: if prop.HasField("property_binding"): out.append( - f"{prop.property_binding.name}: {format_js_type_for_python(prop.property_binding.type)}" + f"{prop.property_binding.name}: {format_js_type_for_python(prop.property_binding.type)}={format_js_type_default_value_for_python(prop.property_binding.type)}" ) if prop.HasField("event_binding"): out.append( - f"on_{snake_case(prop.event_binding.event_name)}: Callable[[{prop.event_binding.event_name}Event], Any]" + f"on_{snake_case(prop.event_binding.event_name)}: Callable[[{prop.event_binding.event_name}Event], Any]|None=None" ) if spec.HasField("content"): out.append( - f"{spec.content.name}: {format_js_type_for_python(spec.content.type)}" + f"{spec.content.name}: {format_js_type_for_python(spec.content.type)}={format_js_type_default_value_for_python(spec.content.type)}" ) return ", ".join(out) @@ -163,8 +185,9 @@ def generate_py_proto_callsite(spec: pb.ComponentSpec) -> str: if prop.HasField("property_binding"): out.append(f"{prop.property_binding.name}={prop.property_binding.name}") if prop.HasField("event_binding"): + # on_mat_checkbox_change_handler_id=handler_type(on_mat_checkbox_change) if on_mat_checkbox_change else '' out.append( - f"on_{snake_case(prop.event_binding.event_name)}_handler_id=handler_type(on_{snake_case(prop.event_binding.event_name)})" + f"on_{snake_case(prop.event_binding.event_name)}_handler_id=handler_type(on_{snake_case(prop.event_binding.event_name)}) if on_{snake_case(prop.event_binding.event_name)} else ''" ) if spec.HasField("content"): out.append(f"{spec.content.name}={spec.content.name}") @@ -181,9 +204,9 @@ def generate_ts_event_methods(spec: pb.ComponentSpec) -> str: def generate_ts_event_method(event_binding: pb.EventBinding) -> str: return f""" - handle{event_binding.event_name}(event: {event_binding.event_name}): void {{ + on{event_binding.event_name}(event: {event_binding.event_name}): void {{ const userEvent = new UserEvent(); - userEvent.set{format_js_type(event_binding.props[0].type).capitalize()}(event.{event_binding.props[0].key}) + userEvent.set{format_js_type_for_proto(event_binding.props[0].type).capitalize()}(event.{event_binding.props[0].key}) userEvent.setHandlerId(this.config().getOn{event_binding.event_name}HandlerId()) userEvent.setKey(this.key); this.channel.dispatch(userEvent); @@ -209,6 +232,24 @@ def format_js_type_for_python(type: pb.JsType.ValueType) -> str: raise Exception("not yet handled", type) +def format_js_type_default_value_for_python(type: pb.JsType.ValueType) -> Any: + if type == pb.JsType.BOOL: + return False + elif type == pb.JsType.STRING: + return "''" + else: + raise Exception("not yet handled", type) + + +def format_js_type_for_proto(type: pb.JsType.ValueType) -> str: + if type == pb.JsType.BOOL: + return "bool" + elif type == pb.JsType.STRING: + return "string" + else: + raise Exception("not yet handled", type) + + def format_element_prop(prop: pb.ElementProp) -> str: if prop.HasField("event_binding"): return f"""({prop.key})="on{prop.event_binding.event_name}($event)\"""" diff --git a/mesop/__init__.py b/mesop/__init__.py index 4da86afa5..f148788ed 100644 --- a/mesop/__init__.py +++ b/mesop/__init__.py @@ -4,9 +4,6 @@ # REF(//scripts/gen_component.py):insert_component_import_export from mesop.components.box.box import box as box from mesop.components.button.button import button as button -from mesop.components.checkbox.checkbox import ( - CheckboxEvent as CheckboxEvent, -) from mesop.components.checkbox.checkbox import ( checkbox as checkbox, ) diff --git a/mesop/components/checkbox/checkbox.ng.html b/mesop/components/checkbox/checkbox.ng.html index ab0707b2a..f7a2b536a 100644 --- a/mesop/components/checkbox/checkbox.ng.html +++ b/mesop/components/checkbox/checkbox.ng.html @@ -1,3 +1,3 @@ - + {{config().getLabel()}} diff --git a/mesop/components/checkbox/checkbox.proto b/mesop/components/checkbox/checkbox.proto index 586489ab4..b3c3ed09e 100644 --- a/mesop/components/checkbox/checkbox.proto +++ b/mesop/components/checkbox/checkbox.proto @@ -1,9 +1,10 @@ + syntax = "proto3"; package mesop.components.checkbox; message CheckboxType { - string label = 1; - string on_update_handler_id = 2; - bool default_value = 3; + bool value = 1; + string on_mat_checkbox_change_handler_id = 2; + string label = 3; } diff --git a/mesop/components/checkbox/checkbox.py b/mesop/components/checkbox/checkbox.py index b361836d7..e9a4f2bf6 100644 --- a/mesop/components/checkbox/checkbox.py +++ b/mesop/components/checkbox/checkbox.py @@ -13,42 +13,38 @@ @dataclass -class CheckboxEvent(MesopEvent): +class MatCheckboxChangeEvent(MesopEvent): checked: bool +register_event_mapper( + MatCheckboxChangeEvent, + lambda event, key: MatCheckboxChangeEvent( + key=key, + checked=event.bool, + ), +) + + @validate_arguments def checkbox( *, - label: str, - on_update: Callable[[CheckboxEvent], Any], - default_value: bool = False, key: str | None = None, + value: bool = False, + on_mat_checkbox_change: Callable[[MatCheckboxChangeEvent], Any] | None = None, + label: str = "", ): """ - Creates a checkbox component with a specified label and update action. - - Args: - label (str): The label for the checkbox. - on_update (Callable[..., Any]): The function to be called when the checkbox is updated. - - The function appends the created checkbox component to the children of the current node in the runtime context. + TODO_doc_string """ insert_component( key=key, type_name="checkbox", proto=checkbox_pb.CheckboxType( + value=value, + on_mat_checkbox_change_handler_id=handler_type(on_mat_checkbox_change) + if on_mat_checkbox_change + else "", label=label, - on_update_handler_id=handler_type(on_update), - default_value=default_value, ), ) - - -register_event_mapper( - CheckboxEvent, - lambda userEvent, key: CheckboxEvent( - key=key, - checked=userEvent.bool, - ), -) diff --git a/mesop/components/checkbox/checkbox.ts b/mesop/components/checkbox/checkbox.ts index 99d42ca50..af6cca065 100644 --- a/mesop/components/checkbox/checkbox.ts +++ b/mesop/components/checkbox/checkbox.ts @@ -1,15 +1,15 @@ -import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {MatCheckboxModule, MatCheckboxChange} from '@angular/material/checkbox'; +import {Component, Input} from '@angular/core'; import { + UserEvent, Key, Type, - UserEvent, } from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; import {CheckboxType} from 'mesop/mesop/components/checkbox/checkbox_jspb_proto_pb/mesop/components/checkbox/checkbox_pb'; import {Channel} from '../../web/src/services/channel'; -import {MatCheckboxChange, MatCheckboxModule} from '@angular/material/checkbox'; @Component({ - selector: 'mesop-checkbox', + // selector: 'mesop-{component-name}', templateUrl: 'checkbox.ng.html', standalone: true, imports: [MatCheckboxModule], @@ -18,7 +18,7 @@ export class CheckboxComponent { @Input({required: true}) type!: Type; @Input() key!: Key; private _config!: CheckboxType; - value = false; + value!: boolean; constructor(private readonly channel: Channel) {} @@ -26,17 +26,17 @@ export class CheckboxComponent { this._config = CheckboxType.deserializeBinary( this.type.getValue() as unknown as Uint8Array, ); - this.value = this._config.getDefaultValue(); + this.value = this._config.getValue(); } config(): CheckboxType { return this._config; } - handleCheckboxChange(event: MatCheckboxChange) { + onMatCheckboxChange(event: MatCheckboxChange): void { const userEvent = new UserEvent(); userEvent.setBool(event.checked); - userEvent.setHandlerId(this.config().getOnUpdateHandlerId()!); + userEvent.setHandlerId(this.config().getOnMatCheckboxChangeHandlerId()); userEvent.setKey(this.key); this.channel.dispatch(userEvent); } diff --git a/mesop/components/checkbox/e2e/checkbox_app.py b/mesop/components/checkbox/e2e/checkbox_app.py index e3311475b..450c6f59f 100644 --- a/mesop/components/checkbox/e2e/checkbox_app.py +++ b/mesop/components/checkbox/e2e/checkbox_app.py @@ -16,7 +16,7 @@ def on_update(event: me.CheckboxEvent): def app(): state = me.state(State) me.checkbox( - label="checkbox", on_update=on_update, default_value=state.checked + label="checkbox", on_mat_checkbox_change=on_update, value=state.checked ) if state.checked: me.text(text="is checked") diff --git a/mesop/components/checkbox/e2e/checkbox_test.ts b/mesop/components/checkbox/e2e/checkbox_test.ts index 04582ffc2..c959d1dbd 100644 --- a/mesop/components/checkbox/e2e/checkbox_test.ts +++ b/mesop/components/checkbox/e2e/checkbox_test.ts @@ -2,12 +2,13 @@ import {test, expect} from '@playwright/test'; test('test', async ({page}) => { await page.goto('/components/checkbox/e2e/checkbox_app'); - expect(await page.getByText('is not checked').textContent()).toContain( - 'is not checked', - ); - - await page.getByRole('checkbox').check(); expect(await page.getByText('is checked').textContent()).toContain( 'is checked', ); + + await page.getByRole('checkbox').uncheck(); + + expect(await page.getByText('is not checked').textContent()).toContain( + 'is not checked', + ); }); diff --git a/mesop/components/text_input/e2e/text_input_app.py b/mesop/components/text_input/e2e/text_input_app.py index be9692cb1..bf15638b4 100644 --- a/mesop/components/text_input/e2e/text_input_app.py +++ b/mesop/components/text_input/e2e/text_input_app.py @@ -22,7 +22,7 @@ def change_checkbox(event: me.CheckboxEvent): @me.page(path="/components/text_input/e2e/text_input_app") def app(): state = me.state(State) - me.checkbox(label="hide_text_input", on_update=change_checkbox) + me.checkbox(label="hide_text_input", on_mat_checkbox_change=change_checkbox) if not state.hide_text_input: me.text_input( label="simple-text-input", default_value=state.string, on_change=change diff --git a/mesop/examples/simple.py b/mesop/examples/simple.py index 222b84747..3e7d2cc33 100644 --- a/mesop/examples/simple.py +++ b/mesop/examples/simple.py @@ -39,7 +39,9 @@ def main(): me.text(text=f"Selected keys: {state.keys}") for i in range(1000): me.checkbox( - label=f"check {i}?", on_update=checkbox_update, key=f"check={i}" + label=f"check {i}?", + on_mat_checkbox_change=checkbox_update, + key=f"check={i}", ) me.text(text=state.string) diff --git a/mesop/protos/ui.proto b/mesop/protos/ui.proto index 51dace495..5164a0959 100644 --- a/mesop/protos/ui.proto +++ b/mesop/protos/ui.proto @@ -114,6 +114,16 @@ message ComponentSpec { repeated ElementProp props = 3; ContentSpec content = 4; + + // TODO: may need to make this multiple modules + NgModuleSpec ng_module = 5; +} + +message NgModuleSpec { + string import_path = 1; + string module_name = 2; + // Other symbols that need to be imported + repeated string other_symbols = 3; } message ContentSpec {