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 {