Replies: 3 comments 8 replies
-
A couple of thoughts on the shape of the API:
With all my suggestions taken together, the first example would be simplified to this format: const OrderRowModel = ObjectModel
.subType<OrderRow>()
.name('OrderRow') // A string name for debug output
.property('product', StringModel)
.property('number', NumberModel)
.build(); |
Beta Was this translation helpful? Give feedback.
-
For dummies like me, can someone please explain how the above proposals would change the following example? <AutoGrid
service={ProductService}
model={ProductModel}
visibleColumns={['category', 'name', 'supplier.supplierName', 'price']}
/> Also, to clarify, would the above proposal(s) work equally well for all components? For example, can one use the proposed approaches to declare a ComboBox's <ComboBox item-label-path="name" item-value-path="id" items={items} /> |
Beta Was this translation helpful? Give feedback.
-
Dumb questions:
|
Beta Was this translation helpful? Give feedback.
-
After some extensive prototyping, I would like to propose the new design for Hilla models that replaces TypeScript class based definitions with objects defined using a builder API.
In this discussion, let me explain the purpose and some key design choices. Please give your feedback, and feel free to ask questions.
At the same time, here is my prototype branch for those who want to play with this idea themselves: https://github.com/vaadin/hilla/tree/proto/model-object-design/packages/ts/model. The test file has some usage examples.
Motivation
Hilla models were originally introduced to support form binding use cases. But recently we started to add more high-level data-oriented frontend developer productivity features to Hilla, such as
AutoGrid
and the upcomingAutoForm
/AutoCrud
, where we also take the data structure and metadata from the models. Some limitations of the current class-based models design became apparent:NamedModel.name
references thename
property of aNamedModel
, andObject.keys(SomeModel)
could iterate the keys. The model classes do not meet this expectation, and require an extra step for these use cases: either instantiation or a prototype access of some sort.Usage
Type description
Models are primarily used as runtime values that describe the underlying type. To illustrate, let us consider the following generated entity structure:
The
name
property of this type could be referenced by indexing:However, TypeScript types do not work as values. You cannot, for example, pass them to some React component props:
For this we need some value that describes the type and has equivalent structure. This is what the models are.
Let us assume that Hilla provides an additional
NamedModel
object for our use case, which simplified structure is:Now you can use both
NamedModel
andNamedModel.name
values as descriptions of their respective types:The key difference here from the existing class-based Hilla models is that you can directly reference a property (
NamedModel.name
), which is not supported with the current class-based design.Creating models
Hilla generates both interfaces and their models for Java entities in endpoints, but sometimes you may not have a Java type. One common example is coming up with a test model for testing a frontend component integrated with Hilla models.
As a base, Hilla provides builtin model values for primitives (
BooleanModel
,NumberModel
,StringModel
), and empty objects (ObjectModel
).You can create an object model using the Hilla-provided
m.from
builder API.Models of optional value (
T | undefined
) and array models (T[]
) can be created by callingm.optional(model)
andm.array(model)
. Models of TypeScript enums can be created withm.enum(enum);
.Object models with primitive properties
Inheritance
Composition
Optionals, arrays, enums
Self-references in objects
Hierarchy location
When you define a property using some model as a value, the builder internally “attaches” it. The resulting property will hold a copy of the given model value with altered metadata, so that the information about the container and the key.
In the end, every model value could tell about its location in the hierarchy: it is either detached (by default) or attached some object property or array item.
In above examples,
AddressModel
andCustomerModel.address
describe the same type (Address
object), but their values are different: the latter is attached by nesting in the"address"
property ofCustomerModel
. The same is also true for the deep nested models (AddressModel.street
andCustomerModel.address.street
).Reference
Now let us take a look at the model object internals.
Base types and values
First, there is a default container for all the detached models:
All the models implement the base interface
IModel<T>
:There is
AbstractModel
value that describesunknown
type following the above interface.Readonly pattern
The model properties are strictly read-only to reject mutation attempts, that are likely to cause errors down the road. Users are expected to create copies or wrap existing models using the Hilla model APIs.
Internal and public properties
To avoid naming conflicts with the user's entity properties, the internal properties are defined using symbols, as illustrated in
IModel<T>
.String properties only occur in object models. They are used to resemble the structure of the user's entity.
The internal properties are non-enumerable, whereas object properties can be enumerated using the regular JS workflow:
for...in
loop orObject.keys()
/Object.entries()
in combination withObject.getPrototypeOf()
for hierarchy traversal.Implementation notes
The
m
model APIs useObject.create
internally to retain the hierarchy in the prototype chain.Here is a rough illustration of the low-level implementation behind the APIs:
Beta Was this translation helpful? Give feedback.
All reactions