Click here to Skip to main content
14,876,076 members
Articles / Programming Languages / Typescript
Article
Posted 5 Jul 2020

Tagged as

Stats

3.7K views
11 downloads
1 bookmarked

Exploring Proxy to Achieve DOM Intellisense with TypeScript for Element Binding, Two Way Data Binding, Events, and More

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
5 Jul 2020CPOL20 min read
Learning about how the Proxy class can be used to, among other things, eliminate element ID string literals and with TypeScript, provide Intellisense for HTML elements
I have found it a useful exploration of how to leverage the Proxy type to bind class properties to HTML elements, achieve two way binding, subscribe to UI events, and so forth, all using actual "edit-time" types for type safety and Intellisense support - as in, eliminate literal strings for referencing DOM elements by their IDs.

Image 1

Contents

Introduction

I despise two things about front-end development:

  1. Element IDs are string literals
  2. JavaScript code in my HTML
  3. JavaScript
  4. Actually anything having to do with front-end development, but that's life

Oh wait! That's four things.

When I started writing the code for this article, I ended up experiencing something that Theory U describes as "Leading From the Future As It Emerges." Riiiight. Nonetheless, this was my experience:

Image 2

So my "future" was discovering that the code I'm about to present here is, well, not what I would actually want to use, now that the future as arrived in the present and by the time I finished writing this article, I realized there's a lot of things I would do differently! Regardless, I have found it a useful exploration of how to leverage the Proxy type to bind class properties to models, achieve two way binding, subscribe to UI events, and so forth, all using actual "edit-time" types for type safety and Intellisense support - as in, no strings referencing DOM elements by their IDs. Thus "IX" is born, which is short for "Interacx", which was a WinForm suite of tools that I created a long time ago to automate data manipulation without using an ORM. I decided to repurpose the name since WinForm applications are, well, passe, and the reality is that the thing I despise, writing web apps, is where it's app, I mean, at. And for your endless amusement, I decided to use some Vue examples as comparison to the implementation I've developed here using proxies.

Pros

Working with the code I've developed here, I find several advantages:

  1. I'm not hardcoding DOM ID string literals.
  2. I'm able to leverage the type safety of TypeScript.
  3. Being able to refer to DOM elements as object properties leverages Visual Studio's Intellisense.
  4. It's really easy to wire up events and bindings.
  5. It was quite easy to write unit tests - in fact, the unit tests are one of the more interesting aspects of this code, in my opinion.
  6. I'm not putting "declarative code" in the HTML
    1. The HTML remains completely clean.
    2. The business logic is implemented in code.
    3. You don't have to inspect both code and HTML to figure out what in the world is actually going on.
  7. Point #6
  8. Point #6
  9. Point #6

I cannot reiterate enough how important, at least to me, point #6 is. With a large web application, I have pulled my hair out bouncing between code and markup to figure out what the conditions, loops, and rendering is, and it is a frustrating experience. To me, the idea of including declarative syntax at the UI level that is driven by effectively business data/rules is bad, no horrible, design. It's why I don't use Razor or similar rendering engines. I personally think that arcane custom tags in the HTML, "if" and "loop" tags, etc., to control UI rendering is one of the worst ideas to come out of so-called modern web development.

Cons

So let's be realistic:

  1. The syntax requires a specific mapping between the DOM element ID and the object's property name.
  2. Proxies are slower.
  3. The code to work with proxies is highly specialized.
  4. The code to work with arrays is bizarre.
  5. The code here is really incomplete with regards to all the DOM attributes, properties, and events that could be handled.
  6. I have no idea whether the code here is actually robust enough to handle #4.
  7. I have yet to explore whether this concept works well with third party widget libraries, my favorite being jqWidgets.
  8. The "future" arrived rather late, basically by the time I was done writing this article.

And I really doubt anyone is going to say, "ooh, let's use IX to build a major website", except perhaps for me!

So Why Bother?

  1. I like to explore different ways to solve the warts of web development.
  2. I haven't come across anyone else attempting this.
  3. It's quite interesting to learn about proxies.
  4. This was fun!

What is a Proxy?

A Proxy, at least in JavaScript, is an object that replaces your object and lets you intercept the "get" and "set" methods. Read more about the Proxy object here.

A Simple Example

A simple demonstration will suffice. First, a simple proxy stub that just does the get/set operations with console logging:

JavaScript
private myProxyHandler = {
  get: (obj, prop) => {
    console.log(`get ${prop}`);

    return obj[prop];
  },

  set: (obj, prop, val) => {
    console.log(`set ${prop} to ${val}`);
    obj[prop] = val;

    // Return true to accept change.
    return true;
  }
}

And a simple test case:

JavaScript
let proxy = new Proxy({}, this.myProxyHandler);
proxy.foo = 1;
let foo = proxy.foo;
console.log(`foo = ${foo}`);

and the output:

set foo to 1
get foo
foo = 1

Ahh, feel the power! The world is now mine!

A DOM Example

Now let's do something a little more interesting. We'll create a class with a property whose name matches a DOM element. The DOM element, with a label because input elements should have labels:

HTML
<div class="inline marginTop5">
  <div class="inline label">Name:</div>
  <div class="inline"><input id="name"/></div>
</div>

Image 3

Exciting!

Now the class:

JavaScript
class NameContainer {
  name: string;
}

And the new proxy:

JavaScript
private valueProxy = {
  get: (obj, prop) => {
    console.log(`get ${prop}`);

    return obj[prop];
  },

  set: (obj, prop, val) => {
    console.log(`set ${prop} to ${val}`);

    let el = document.getElementById(prop) as HTMLInputElement;
    el.value = val;
    obj[prop] = val;

    // Return true to accept change.
    return true;
  }
}

Notice the only thing I've added is this:

JavaScript
let el = document.getElementById(prop) as HTMLInputElement;
el.value = val;

Here, the assumption is that the property name is the element ID!

Now we can set the value and it proxies to setting both the object's name property and the DOM value property:

JavaScript
let nc = new Proxy(new NameContainer(), this.valueProxy);
nc.name = "Hello World!";

The result is:

Image 4

What if I type something in and I want to see that value when I "get" the name property? Easy enough, the getter changes to this:

JavaScript
get: (obj, prop) => {
  console.log(`get ${prop}`);

  let el = document.getElementById(prop) as HTMLInputElement;
  let val = el.value;
  obj[prop] = val;

  return obj[prop];
},

We can test the code by simulating a change the user made:

JavaScript
let nc = new Proxy(new NameContainer(), this.valueProxy);
nc.name = "Hello World!";

// Simulate the user having changed the input box:
let el = document.getElementById("name") as HTMLInputElement;
el.value = "fizbin";

let newName = nc.name;
console.log(`The new name is: ${newName}`);

and in the console log, we see:

set name to Hello World!
get name
The new name is: fizbin

It's important to note that obj[prop] = val makes the assignment on the non-proxy'd object, therefore the proxy setter does not get called.

The Point Being...

  1. I'm using types (and therefore Intellisense) to get/set the DOM element value.
  2. I've eliminated the string literal for the name by assuming that the name of the class property is the same as the element ID.

Snazzy! One small step for Marc, one giant leap for better front-end development! Unfortunately, getting to the moon requires a whole lot more effort, infrastructure, time, and a lot of disasters along the way (a pause here to recognize the lives that have been lost in space exploration, as I don't want to appear flippant about "disasters.")

So, let's start the journey down the slope of the U!

About the Code

The code is TypeScript with simple HTML and CSS, implemented in VS2017 solution.

The source code can also be found at https://github.com/cliftonm/IX.

There are two HTML pages to play with:

  1. index.html is the demo page.
  2. Tests/IntegrationTests.html runs the integration tests.

Simple Data Binding of Inner HTML

Let's start with simple data binding of the inner HTML associated with a DIV.

The Vue Way

JavaScript
<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
   message: 'Hello Vue!'
  }
})

What I don't like:

  1. The "Mustache" {{ }} usage.
  2. The #app.
  3. The whole data object thing.

The IX Way

HTML
<div id="app"></div>

let form = IX.CreateNullProxy(); // No associated view model.
form.app = "Hello Interacx!";

Image 5

That's it!

Reactive Behavior

The next example is displaying some realtime computed value as part of a SPAN title.

The Vue Way

HTML
<div id="app-2">
  <span v-bind:title="message">
    Hover your mouse over me for a few seconds
    to see my dynamically bound title!
  </span>
</div>

var app2 = new Vue({
  el: '#app-2',
  data: {
    message: 'You loaded this page on ' + new Date().toLocaleString()
  }
})

The IX Way

<span id="mySpan">However your mouse over me for a few seconds 
to see the dynamically bound title!</span>

class HoverExample {
  mySpan = {
    attr: { title: "" }
  };

  onMySpanHover = new IXEvent();
}

let form = IX.CreateProxy(new HoverExample());
form
  .onMySpanHover
  .Add(() => 
    hform.mySpan.attr.title = `You loaded this page on ${new Date().toLocaleString()}`);

More verbose, but the benefit is that you're using a repeatable pattern of using a multicast event handler. I did have an implementation where I could just set the title as a function, but I didn't like the one-off implementation behind the scenes that this required.

Conditionals

I also really don't like to make a mess of the markup with declarative code elements.

The Vue Way

HTML
<div id="app-3">
  <span v-if="seen">Now you see me</span>
</div>

var app3 = new Vue({
  el: '#app-3',
  data: {
    seen: true
  }
})

The IX Way

In IX, conditional behaviors are implemented through the event mechanism, usually to manipulate element attributes. Diverging slightly from the Vue example above, note the addition of two buttons to toggle the visibility of the SPAN:

HTML
<span id="seen">Now you see me...</span>
 <!-- Two ways to declare a button -->
<button id="show">Show</button>
<input id="hide" type="button" value="Hide" />

class VisibilityExample {
  seen = {
    attr: { visible: true }
  };

  onShowClicked = new IXEvent().Add((_, p) => p.seen.attr.visible = true);
  onHideClicked = new IXEvent().Add((_, p) => p.seen.attr.visible = false);
}

IX.CreateProxy(new VisibilityExample());

These are wired up to two buttons, hence the event handlers.

Here:

  1. We have a consistent way of manipulating element attributes.
  2. Intellisense works perfectly in Visual Studio.
  3. No "string" element name.

Loops

The Vue Way

HTML
<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>

var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: 'Learn JavaScript' },
      { text: 'Learn Vue' },
      { text: 'Build something awesome' }
    ]
  }
})

The IX Way

HTML
<ol id="someList"></ol>

class ListExample {
  someList: string[] = ["Learn Javascript", "Learn IX", "Wear a mask!"];
}

IX.CreateProxy(new ListExample());

Result:

Image 6

Given that most lists come from a data source rather being hard coded:

HTML
<ol id="someList"></ol>

class ListExample {
  someList: string[] = [];
}

let listForm = IX.CreateProxy(new ListExample());

listForm.someList.push("Learn Javascript");
listForm.someList.push("Learn IX");
listForm.someList.push("Wear a mask!");

Or:

JavaScript
let listForm = IX.CreateProxy(new ListExample());
let items = ["Learn Javascript", "Learn IX", "Wear a mask!"];
listForm.someList = items;

Button Clicks

The Vue Way

HTML
<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">Reverse Message</button>
</div>

var app5 = new Vue({
  el: '#app-5',
  data: {
  message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

The IX Way

HTML
<div>
  <p id="message"></p>
  <button id="reverseMessage">Reverse Message</button>
</div>

class ReverseExample {
  message = "Hello From Interacx!";
  onReverseMessageClicked = new IXEvent()
    .Add((_, p: ReverseExample) => p.message = p.message.split('').reverse().join(''));
}

IX.CreateProxy(new ReverseExample());

Again, notice:

  1. No "Mustache" {{ }} syntax required.
  2. No "#id" string to identify the element ID.
  3. The event mechanism, being multicast, allows us to wire up more than one event (not illustrated, but that's point of using events.)

Image 7

After clicking on the button:

Image 8

Data Conversion

The following example is similar to Vue's .number attribute, but the actual implementation is much more general purpose.

Consider this UI:

Image 9

And the markup (CSS and extraneous DIVs removed for readability):

HTML
X:
<input id="x" class="fieldInputSmall" />
Y:
<input id="y" class="fieldInputSmall" />

Here, we do not want the strings "1" and "2" to sum to "12", so we implement converters:

JavaScript
class InputForm {
  x: number;
  y: number;

  onXChanged = new IXEvent();
  onYChanged = new IXEvent();

  // Converters, so 1 + 2 != '12'
  onConvertX = x => Number(x);
  onConvertY = y => Number(y);

  Add = () => this.x + this.y;
}

class OutputForm {
  sum: number;
}

And the events are wired up like this:

JavaScript
let inputForm = IX.CreateProxy(new InputForm());
let outputForm = IX.CreateProxy(new OutputForm());

inputForm.onXChanged.Add(() => outputForm.sum = inputForm.Add());
inputForm.onYChanged.Add(() => outputForm.sum = inputForm.Add());

Behind the scenes, the input box text is converted to a Number with the onConvertX and onConvertY converters, and the rest is handled by the standard data binding of the properties for setting sum to the values of x and y.

Also, notice how you can create classes as containers to sections of the HTML. We could easily have put sum in the InputForm, but instead I wanted to illustrate how to use a separate container object, OutputForm, as a way of compartmentalizing the properties into separate containers.

Two Way Binding

We've already seen in the examples above binding between the view and the model. One of Vue's examples is direct update of one element based on the realtime update of an input element. While I can't think of a real-life example where one would need this, real-time updating, say of a filter criteria, is definitely useful, so we'll start with the Vue example:

The Vue Way

HTML
<div id="app-6">
  <p>{{ message }}</p>
    <input v-model="message">
  </div>

var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})

The IX Way

This is already easily accomplished with events:

JavaScript
First Name:
<p id="message2">/p>
<input id="input2"/>

class BidirectionalExample {
  message2: string = "";
  input2: string = "";

  onInput2KeyUp = new IXEvent().Add((v, p: BidirectionalExample) => p.message2 = v);
}

IX.CreateProxy(new BidirectionalExample());

Image 10

However, to make this more "Vue-ish", we can do:

JavaScript
class BidirectionalExample {
  message2 = new IXBinder({ input2: null });
  input2: string = "";

Here, we are specifying the "from" element as the key and any "value" of the key. What displeases me about this is that the key cannot be implemented in a way that leverages Intellisense and type checking. The best we can do is runtime checking that the "from" binder element exists. So at this point, specifying the "bind from" property as a string almost makes sense. Instead, I opted for this implementation:

JavaScript
class BidirectionalExample {
  input2: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  onInput2KeyUp = new IXEvent().Add((v, p: BidirectionalExample) => p.message2 = v);
}

Which is somewhat lame as well but has the advantage of supporting Intellisense, albeit the property your binding to must already be declared previously. Behind the scenes, we have a very simple implementation to extract the name, by converting the function into a string:

JavaScript
public static nameof<TResult>(name: () => TResult): string {
  let ret = IX.RightOf(name.toString(), ".");

  return ret;
}

Sadly, that's seems to be the best we can do with JavaScript unless you want to use something like ts-nameof, which I do not because ts-nameof is a compile-time transformation, and I do not want the developer that uses this library to have to go through hoops to get this to work.

We can also bind the same source to different targets:

HTML
<p>
  <label id="message2"></label>
  <label id="message3"></label>
</p>
<input id="input2" />

class BidirectionalExample {
  input2: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  message3 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
}

As well as different sources to the same target:

HTML
<p>
  <label id="message2"></label>
  <label id="message3"></label>
</p>
<input id="input2" />
<input id="input3" />

class BidirectionalExample {
  input2: string = "";
  input3: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  message3 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) })
               .Add({ bindFrom: IX.nameof(() => this.input3) });
}

Here, typing in the left edit box sets messages 2 & 3:

Image 11

Typing in the right edit box sets message 3:

Image 12

But as I said earlier, doing this kind of binding really doesn't make much sense. Typically, a transformation does something "useful", so we have this contrived example:

JavaScript
class BidirectionalExample {
  input2: string = "";
  input3: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  message3 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) }).Add({
    bindFrom: IX.nameof(() => this.input3), 
    op: v => v.split('').reverse().join('') 
  });

// onInput2KeyUp = new IXEvent().Add((v, p: BidirectionalExample) => p.message2 = v);
}

and thus we get:

Image 13

Checkboxes

Binding Checkbox State

The Vue Way

Vue has an elegant demonstration of binding the checkbox state to the label:

HTML
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>

The IX Way

Given:

HTML
<input id="checkbox" type="checkbox" />
<label id="ckLabel" for="checkbox"></label>

We continue to follow the pattern of using TypeScript classes and properties:

JavaScript
class CheckboxExample {
  checkbox: boolean = false;
  ckLabel = new IXBinder({ bindFrom: IX.nameof(() => this.checkbox) });
}

IX.CreateProxy(new CheckboxExample());

or, because the nameof syntax above is clumsy and we don't have a real "nameof" operator in JavaScript by the time the code is transpiled, so we have to revert to string literals in this case:

JavaScript
class CheckboxExample {
  checkbox: boolean = false;
  ckLabel = new IXBinder({ bindFrom: "checkbox" });
}

IX.CreateProxy(new CheckboxExample());

Image 14

Image 15

Or we can wire up the click event:

JavaScript
class CheckboxExample {
  checkbox: boolean = false;
  ckLabel: string = "Unchecked";

  onCheckboxClicked = 
    new IXEvent().Add(
      (_, p: CheckboxExample) => 
          p.ckLabel = p.checkbox ? "Checked" : "Unchecked");
}

IX.CreateProxy(new CheckboxExample());

Image 16

Image 17

Binding Checkbox Values

The View Way

HTML
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<br>
<span>Checked names: {{ checkedNames }}</span>

Note that the span text includes the array brackets:

Image 18

The IX Way

Given:

HTML
<input id="jane" value="Jane" type="checkbox" />
<label for="jane">Jane</label>
<input id="mary" value="Mary" type="checkbox" />
<label for="mary">Mary</label>
<input id="grace" value="Grace" type="checkbox" />
<label for="grace">Grace</label>
<br />
<label id="ckNames"></label>

We implement the container object with a special array binding (because the properties don't exist in the class, I can't use the "nameof" kludge, so the "ID"s are, sadly, string literals.) Of course, in the next example, I do have properties for the checkboxes, but I still used the string literals!

JavaScript
class CheckboxListExample {
  ckNames = IXBinder.AsArray(items => items.join(", "))
    .Add({ bindFrom: "jane", attribute: "value" })
    .Add({ bindFrom: "mary", attribute: "value" })
    .Add({ bindFrom: "grace", attribute: "value" });
}

IX.CreateProxy(new CheckboxListExample());

And we get:

Image 19

Notice that we did not initialize properties with the checkbox state! If we do this:

JavaScript
class CheckboxListExample {
  jane: boolean = false;
  mary: boolean = false;
  grace: boolean = false;
  ckNames = IXBinder.AsArray(items => items.join(", "))
    .Add({ bindFrom: "jane", attribute: "value" })
    .Add({ bindFrom: "mary", attribute: "value" })
    .Add({ bindFrom: "grace", attribute: "value" });
}

let ckListExample = IX.CreateProxy(new CheckboxListExample());

We can programmatically set the check state:

JavaScript
ckListExample.jane = true;
ckListExample.mary = true;

and we see:

Image 20

So one thing we note here is that the property referring to the HTML element is associated with the checked attribute of the element. That is an artifact of how IX is coded, and actually points out an interesting problem -- the object property maps to only one attribute of the DOM element, and IX is very opinionated as to what that DOM element should be, depending on what the element is!

Radio Buttons

The View Way

This example binds the value of the radio button to the span:

HTML
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>Picked: {{ picked }}</span>

The IX Way

Given:

HTML
<input id="marc" value="Marc" type="radio" name="group1" />
<label for="marc">Marc</label>
<input id="chris" value="Chris" type="radio" name="group1" />
<label for="chris">Chris</label>
<br />
<label id="rbPicked"></label>

We add two binders, whichever one is clicked becomes the one whose binder event is fired. Again, note in this example, I'm not using the "nameof" syntax because in this case the property doesn't exist!

JavaScript
class RadioExample {
  rbPicked = new IXBinder({ bindFrom: "marc", attribute: "value" })
    .Add({ bindFrom: "chris", attribute: "value" });
}

IX.CreateProxy(new RadioExample());

thus updating to the current radio button:

Image 21

Image 22

And if we want to programmatically set the radio button state, define the properties:

JavaScript
class RadioExample {
  marc: boolean = false;
  chris: boolean = false;
  rbPicked = new IXBinder({ bindFrom: "marc", attribute: "value" })
    .Add({ bindFrom: "chris", attribute: "value" });
}

and after proxy initialization, set the state:

JavaScript
let rbExample = IX.CreateProxy(new RadioExample());
rbExample.chris = true;

Image 23

ComboBoxes

The Vue Way

HTML
<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<span>Selected: {{ selected }}</span>

The IX Way

Given:

JavaScript
<select id="selector">
  <option selected disabled>Please select one</option>
  <option value="1">A</option>
  <option value="2">B</option>
  <option value="3">C</option>
</select>
<br />
<span id="selection"></span>

and the container class:

JavaScript
class ComboboxExample {
  selector = new IXSelector();
  selection: string = "";

  onSelectorChanged = 
    new IXEvent().Add((_, p) => 
      p.selection = `Selected: ${p.selector.text} with value ${p.selector.value}`);
}

IX.CreateProxy(new ComboboxExample());

We then see:

Image 24

and after selection:

Image 25

Note that the selector property, implemented as an IXSelector, contains two properties, text and value, for the selected item.

We can also initialize the options programmatically. Given:

HTML
<select id="selector2"></select>
<br />
<span id="selection2"></span>

and:

JavaScript
class ComboboxInitializationExample {
  selector2 = new IXSelector().Add
     ({ selected:true, disabled: true, text: "Please select one" })
    .Add({ value: 12, text: "AAA" })
    .Add({ value: 23, text: "BBB" })
    .Add({ value: 34, text: "CCC" });

  selection2: string = "";

  onSelector2Changed = new IXEvent().Add((_, p) => 
  p.selection2 = `Selected: ${p.selector2.text} with value ${p.selector2.value}`);
}

 let cb = IX.CreateProxy(new ComboboxInitializationExample());

We see:

Image 26

And programmatically set the selection with the option value:

JavaScript
cb.selector2.value = 34;

Image 27

or with the option text:

JavaScript
cb.selector2.text = "AAA";
JavaScript
<img border="0" height="45" src="5272881/select5.png" width="182" />

Or add to the list of options:

JavaScript
cb.selector2.options.push({ text: "DDD", value: 45 });

Image 28

Or remove the option item:

JavaScript
cb.selector2.options.pop();

Image 29

Or change an option's text and value:

JavaScript
cb.selector2.options[2] = { text: "bbb", value: 999 };

Image 30

Implementation Patterns

IX requires that class properties match the DOM element ID and that event handlers have specific signatures.

ID and Class Property Name

Image 31

KeyUp, Changed, and Convert Events

Image 32

Notice:

The event name uses the property name with the first letter capitalized, so firstName becomes FirstName.

Supported Events

KeyUp

on[Prop]KeyUp - realtime key up events

Changed

on[Prop]Changed - element loses focus

This event applies to text, radio and checkbox inputs and "select" (combobox) elements.

Convert

onConvert[Prop] - if defined, executes the function before the KeyUp and Changed events fire.

Hover

on[Prop]Hover - if defined and the property has the signature:

JavaScript
{
  attr: { title: "" }
};

This will set the element's title on mouse hover.

Integration Tests

Image 33

We can easily test the behavior of IX by directly inspecting DOM elements after model changes, and vice versa. And I prefer to use the phrase "integration test" rather than "unit test" because we're not testing low level functions in the IX library -- we are testing the integration of the DOM elements with object properties.

The HTML for the test cases is simple:

HTML
<div id="testResults" class="inline" style="min-width:600px">
  <ol id="tests"></ol>
</div>

<div id="testDom"></div>

We have an ordered list for the test results, and a div in which we place the HTML required for each test.

The Test Runner

The tests actually use IX to manipulate the test results, and direct DOM manipulation to simulate UI changes. The runner looks like this:

JavaScript
let testForm = IX.CreateProxy(new TestResults());
let idx = 0;

tests.forEach(test => {
  // Get just the name of the test function.
  let testName = IX.LeftOf(test.testFnc.toString(), "(");

  // The ID will start with a lowercase letter
  let id = IX.LowerCaseFirstChar(testName);

  // Push a template to OL, where the template value is simply the test name, 
  // to the test results ordered list.
  testForm.tests.push(IXTemplate.Create({ value: testName, id: id }));

  // Create an object with the id and proxy it. 
  // This will match the id of the template we just created, so we can set its style.
  // This is a great example of not actually needing to create a class, which is really
  // just a dictionary.
  let obj = {};

  // The classList here allows us to set the test LI element style class 
  // to indicate success/failure of the test.
  obj[id] = { classList: new IXClassList() }; 
  let testProxy = IX.CreateProxy(obj);

  // Create the DOM needed for the test.
  this.CreateTestDom(testForm, test.dom);

  // Run the test and indicate the result.
  this.RunTest(testForm, idx, testProxy, test, id);

  // Remove the DOM needed for the test.
  this.RemoveTestDom(testForm);

  ++idx;
});

And we have these three helper functions:

JavaScript
CreateTestDom(testForm: TestResults, testDom: string): void {
  testForm.testDom = testDom || "";
}

RemoveTestDom(testForm: TestResults, ): void {
  testForm.testDom = "";
}

RunTest(testForm: TestResults, idx:number, testProxy: object, test, id: string): void {
  let passFail = "pass";

  try {
    test.testFnc(test.obj, id);
  } catch (err) {
    passFail = "fail";
    let template = testForm.tests[idx];
    template.SetValue(`${template.value} => ${err}`);
  }

  testProxy[id].classList.Add(passFail);
}

A passing test is indicated in green, a failing test in red, along with the error message.

CSS
.pass {
  color: green;
}

.fail {
  color: red;
}

So for example, we can test that failure is handled:

JavaScript
static ShouldFail(obj): void {
  throw "Failed!!!";
}

And we see:

Image 34

Defining the Tests

The tests are defined as an array of objects that specify:

  1. The test to be run.
  2. The "object" being manipulated in the test.
  3. The HTML to support the test.

Like this:

JavaScript
let tests = [
  // { testFnc: IntegrationTests.ShouldFail },
  { testFnc: IntegrationTests.InputElementSetOnInitializationTest, 
    obj: { inputTest: "Test" }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.InputElementSetOnAssignmentTest, 
    obj: { inputTest: "" }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.InputSetsPropertyTest, 
    obj: { inputTest: "" }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.ListInitializedTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.ReplaceInitializedTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.ChangeListItemTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.PushListItemTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.PopListItemTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  {
    testFnc: IntegrationTests.ButtonClickTest,
    obj: { clicked: false, onButtonClicked : new IXEvent().Add((_, p) => p.clicked = true)},
    dom: "<button id='button'></button>"
  },
  {
    testFnc: IntegrationTests.OnlyOneClickEventTest,
    obj: { clicked: 0, onButtonClicked: new IXEvent().Add((_, p) => p.clicked += 1) },
    dom: "<button id='button'></button>"
  },
  {
    testFnc: IntegrationTests.CheckboxClickTest,
    obj: { clicked: false, checkbox: false, 
    onCheckboxClicked: new IXEvent().Add((_, p) => p.clicked = p.checkbox)},
    dom: "<input id='checkbox' type='checkbox'/>"
  },
  {
    testFnc: IntegrationTests.RadioButtonClickTest,
    obj: { clicked: false, checkbox: false, 
    onRadioClicked: new IXEvent().Add((_, p) => p.clicked = p.radio) },
    dom: "<input id='radio' type='radio'/>"
  },
  {
    testFnc: IntegrationTests.ConvertTest,
    obj: { inputTest: "", onConvertInputTest: s => `${s} Converted!` },
    dom: "<input id='inputTest'/>"
  },
  { testFnc: IntegrationTests.VisibleAttributeTest, 
    obj: { inputTest: { attr: { visible: true } } }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.ControlBindingTest, 
    obj: { input: "123", output: new IXBinder({ bindFrom: "input" }) }, 
    dom: "<input id='input'><p id='output'>" },
  { testFnc: IntegrationTests.ControlBindingWithOperationTest, 
    obj: { input: "123", output: new IXBinder({ bindFrom: "input", 
    op: v => `${v} Operated!` }) }, dom: "<input id='input'><p id='output'>" },
  { testFnc: IntegrationTests.ControlBindingAssignmentTest, 
    obj: { input: "", output: new IXBinder({ bindFrom: "input" }) }, 
    dom: "<input id='input'><p id='output'>" },
];

I'm not going to bore you with the actual tests, but I'll point out that in some cases we have to simulate clicking and therefore the test must dispatch the appropriate event, for example:

JavaScript
static ButtonClickTest(obj): void {
  let test = IX.CreateProxy(obj);
  let el = document.getElementById("button") as HTMLButtonElement;
  el.dispatchEvent(new Event('click')); 
  IXAssert.Equal(test.clicked, true);
}

The IXAssert Helper

This class simply wraps the if statement into a one-liner, as I rather dislike if statements for assertions.

JavaScript
export class IXAssert {
  public static Equal(got: any, expected: any): void {
    let b = got == expected;

    if (!b) {
      throw `Expected ${expected}, got ${got}`;
    }
  }

  public static IsTrue(b: boolean): void {
    if (!b) {
      throw "Not true";
    }
  }
}

You Don't Need TypeScript Classes

You should realize from looking at how the tests are implemented that you don't need actual TypeScript classes, you just need an object, like obj: { inputTest: "Test" } - after all, a TypeScript class is a purely development-side construct used for type checking and Intellisense by the IDE. Even a JavaScript class is really just "syntactical sugar of JavaScript's existing prototype-based inheritance." (JavaScript classes)

Behind the Scenes

TypeScript Does Not Mean Runtime Type Reflection

TypeScript is fantastic for ensuring type safety when writing code. However, by the time the code has been transpiled to JavasScript, all that type information that the IDE is using is of course lost. This is unfortunate because there are times in the code that I really wish I had type information. There are some workarounds, such as for native types and classes:

JavaScript
let a = 1;
let b = "foo";
let c = true;
let d = [];
let e = new SomeClass();
[a, b, c, d, e].forEach(q => console.log(q.constructor.name));

let listForm = IX.CreateProxy(new ListExample());

You get:

Image 35

This is useful. However, given this class:

JavaScript
class SomeClass {
  a: number;
  b: string;
}

What that gets transpiled to is simply an empty object {}. So, Object.keys(new SomeClass()) return an empty array []. To determine properties of the class, the properties must be initialized, and they can even be initialized to null or undefined:

JavaScript
class SomeClass {
  a: number = null;
  b: string = undefined;
}

Image 36

Hence, the constraint in IX that you must initialize properties, otherwise the wire-up cannot be made between the class property and the element with the ID of the property name.

Proxy Initialization

JavaScript
public static CreateProxy<T>(container: T): T {
  let proxy = new Proxy(container, IX.uiHandler);
  IX.CreatePropertyHandlers(container, proxy);
  IX.CreateButtonHandlers(container, proxy);
  IX.CreateBinders(container, proxy);
  IX.Initialize(container, proxy);

  return proxy;
}

Besides instantiating the proxy, we can see that several other steps are required:

  1. Special property handlers
  2. Button handlers
  3. Binders
  4. Final initialization

CreatePropertyHandlers

This code is intended to handle attributes, class lists, and event wireups. Events are only wired up once, in case the proxy is re-initialized after class property assignments. To make matters a bit more complicated, specific cases are handled here, such as proxy'ing the attr key to accommodate the custom syntax for assigning attributes to the associated DOM element. The class attribute is handled similarly, creating a proxy for the classList key. A better implementation is discussed in the conclusion. Otherwise, the initial purpose of the function was solely to handle the mouseover, change, and keyup events.

JavaScript
private static CreatePropertyHandlers<T>(container: T, proxy: T) {
  Object.keys(container).forEach(k => {
  let el = document.getElementById(k);
  let anonEl = el as any;

  // If element exists and we haven't assigned a proxy to the container's field, 
  // then wire up the events.
  if (el && !anonEl._proxy) {
    anonEl._proxy = this;

    if (container[k].attr) {
      // Proxy the attributes of the container so we can intercept the setter for attributes
      console.log(`Creating proxy for attr ${k}`);
      container[k].attr = IXAttributeProxy.Create(k, container[k].attr);
    }

    if (container[k].classList) {
      console.log(`Creating proxy for classList ${k}`);
      container[k].classList = IXClassListProxy.Create(k, container[k].classList);
    }

    let idName = IX.UpperCaseFirstChar(el.id);

    // TODO: create a dictionary to handle this.
    let changedEvent = `on${idName}Changed`;
    let hoverEvent = `on${idName}Hover`;
    let keyUpEvent = `on${idName}KeyUp`;

    if (container[hoverEvent]) {
      IX.WireUpEventHandler(el, container, proxy, null, "mouseover", hoverEvent);
    }

    // Change event is always wired up so we set the container's value 
    // when the UI element value changes.
    switch (el.nodeName) {
      case "SELECT":
      case "INPUT":
        // TODO: If this is a button type, then what?
        IX.WireUpEventHandler(el, container, proxy, "value", "change", changedEvent);
      break;
    }

    if (container[keyUpEvent]) {
      switch (el.nodeName) {
        case "INPUT":
          // TODO: If this is a button type, then what?
          IX.WireUpEventHandler(el, container, proxy, "value", "keyup", keyUpEvent);
          break;
        }
      }
    }
  });
}

It should be obvious that this is a very incomplete implementation sufficient for the proof-of-concept.

WireUpEventHandler

The event handler that is attached the event listener implements a custom check for the SELECT HTML element and makes the assumption that the class property has been initialized with an IXSelector instance. This was done so that the selected item's text and value could be assigned on selection to the IXSelector instance. Otherwise, the event handler updates the class' property (as in, the class that has been proxied.) Because "buttons" don't have a property but are just an event, we check if there is actually a property on the DOM that needs to be read and set on the corresponding class property. Lastly, if the class implements an event handler, any multicast events are fired. A custom converter, if defined in the class, is invoked first for non-button events.

JavaScript
private static WireUpEventHandler<T>(el: HTMLElement, container: T, proxy: T, 
  propertyName: string, eventName: string, handlerName: string) {
  el.addEventListener(eventName, ev => {
    let el = ev.srcElement as HTMLElement;
    let oldVal = undefined;
    let newVal = undefined;
    let propName = undefined;
    let handler = container[handlerName];

    switch (el.nodeName) {
      case "SELECT":
        let elSelector = el as HTMLSelectElement;
        let selector = container[el.id] as IXSelector;
        selector.value = elSelector.value;
        selector.text = elSelector.options[elSelector.selectedIndex].text;
        break;

      default:
        // buttons are click events, not change properties.
        if (propertyName) {
          oldVal = container[el.id];
          newVal = el[propertyName];
          propName = el.id;
        }

        let ucPropName = IX.UpperCaseFirstChar(propName ?? "");

        if (propertyName) {
          newVal = IX.CustomConverter(proxy, ucPropName, newVal);
          container[propName] = newVal;
        }

        break;
    }

    if (handler) {
      (handler as IXEvent).Invoke(newVal, proxy, oldVal);
    }
  });
}

Again, enough to implement the proof-of-concept.

CustomConverter

JavaScript
private static CustomConverter<T>(container: T, ucPropName: string, newVal: string): any {
  let converter = `onConvert${ucPropName}`;

  if (container[converter]) {
    newVal = container[converter](newVal);
  }

  return newVal;
}

CreateButtonHandlers

Buttons (and button-like things, like checkboxes and radio buttons) have their own unique requirements. Checkboxes and radio buttons (which are INPUT HTML elements) have a checked property, whereas buttons do not. The proxy'd class must implement the expected "on...." which must be assigned to an IXEvent to support multicast events.

JavaScript
private static CreateButtonHandlers<T>(container: T, proxy: T) {
  Object.keys(container).forEach(k => {
    if (k.startsWith("on") && k.endsWith("Clicked")) {
      let elName = IX.LeftOf(IX.LowerCaseFirstChar(k.substring(2)), "Clicked");
      let el = document.getElementById(elName);
      let anonEl = el as any;

      if (el) {
        if (!anonEl._proxy) {
        anonEl._proxy = this;
      }

      if (!anonEl._clickEventWiredUp) {
        anonEl._clickEventWiredUp = true;

        switch (el.nodeName) {
          case "BUTTON":
            IX.WireUpEventHandler(el, container, proxy, null, "click", k);
            break;

          case "INPUT":
            // sort of not necessary to test type but a good idea, 
            // especially for checkboxes and radio buttons.
            let typeAttr = el.getAttribute("type");

            if (typeAttr == "checkbox" || typeAttr == "radio") {
              IX.WireUpEventHandler(el, container, proxy, "checked", "click", k);
            } else {
              IX.WireUpEventHandler(el, container, proxy, null, "click", k);
            }

            break;
          }
        }
      }
    }
  });
}

CreateBinders

Binders handle real-time events such as keyup as well as when an input element loses focus. Adding to the complexity is the concept that a binder might be associated with multiple checkboxes or radio buttons and bind the list of currently selected items. This is a confusing piece of code, as both array and non-array properties can be bound. It is assumed that if an array is being bound, the array is populated with the selected checkboxes or radio buttons (though technically, a radio button should be exclusive.) Otherwise, the property itself is set with either the checked state or the element's value. Lastly, an optional "op" (operation) can be defined before the value is set on the proxy. Setting the value on the proxy rather than the proxy'd object invokes the proxy setter which can define further behaviors, but ultimately also assigns the value to the original container object.

JavaScript
// We assume binders are created on input elements. Probably not a great assumption.
private static CreateBinders<T>(container: T, proxy: T): void {
  Object.keys(container).forEach(k => {

    if (container[k].binders?.length ?? 0 > 0) {
      let binderContainer = container[k] as IXBinder;
      let binders = binderContainer.binders as IXBind[];

      if (binderContainer.asArray) {
        binders.forEach(b => {
          let elName = b.bindFrom;
          let el = document.getElementById(elName);

          let typeAttr = el.getAttribute("type");

          // Limited support at the moment.
          if (typeAttr == "checkbox" || typeAttr == "radio") {
            el.addEventListener("click", ev => {
              let values: string[] = [];

              // Get all the items currently checked
                binders.forEach(binderItem => {
                  let boundElement = (document.getElementById(binderItem.bindFrom) 
                                      as HTMLInputElement);
                  let checked = boundElement.checked;

                  if (checked) {
                    values.push(boundElement[binderItem.attribute]);
                  }
                });

                let ret = binderContainer.arrayOp(values);
                proxy[k] = ret;
              });
            }
          });
        } else {
          binders.forEach(b => {
            let elName = b.bindFrom;
            let el = document.getElementById(elName);
            console.log(`Binding receiver ${k} to sender ${elName}`);

            let typeAttr = el.getAttribute("type");
  
            if (typeAttr == "checkbox" || typeAttr == "radio") {
              el.addEventListener("click", ev => {
                let boundAttr = b.attribute ?? "checked";
                let v = String((ev.currentTarget as HTMLInputElement)[boundAttr]);
                v = b.op === undefined ? v : b.op(v);
                proxy[k] = v;
              });
            } else {
              // Realtime typing
              el.addEventListener("keyup", ev => {
                let v = (ev.currentTarget as HTMLInputElement).value;
                // proxy[elName] = v; --- why?
                v = b.op === undefined ? v : b.op(v);
                proxy[k] = v;
              });

              // Lost focus, or called when value is set programmatically in the proxy setter.
              el.addEventListener("changed", ev => {
                let v = (ev.currentTarget as HTMLInputElement).value;
                v = b.op === undefined ? v : b.op(v);
                proxy[k] = v;
              });
          }
        });
      }
    }
  });
}

Initialize

This last function started off simple and ended up being more complicated as it needs to handle not just native non-array types, but also arrays and DOM elements like "select":

JavaScript
private static Initialize<T>(container: T, proxy: T): void {
  Object.keys(container).forEach(k => {
    let name = container[k].constructor?.name;

    switch (name) {
      case "String":
      case "Number":
      case "Boolean":
      case "BigInt":
        proxy[k] = container[k]; // Force the proxy to handle the initial value.

        break;

      case "Array":
        // Special handling of arrays that have an initial set of elements 
        // so we don't duplicate the elements.
        // At this point, container[k] IS the proxy (IXArrayProxy) 
        // so we have the issue that the proxy is set to 
        // the array but the UI elements haven't been created. If we just do: 
        // proxy[k] = container[k]; 
        // This will initialize the UI list but push duplicates of the into the array.

        // So, for arrays, we want to create the array proxy 
        // as an empty array during initialization instead,
        // then set the empty proxy to the container, then the container to the proxy.
        if (container[k]._id != k) {
          let newProxy = IXArrayProxy.Create(k, container);
          newProxy[k] = container[k];
          container[k] = newProxy;
        }

        break;

      case "IXSelector":
        // Similar to "Array" above, except we are proxying the IXSelector.options array, 
        // not the container itself.
        if (container[k]._id != k) {
          // Set the element that this IXSelector manages 
          // so we know what to do when value and text are assigned.
          container[k]._element = document.getElementById(k);
          let selector = container[k] as IXSelector;

          // Proxy the options array so we can initialize it as well as push/pop.
          if (selector.options.length > 0) {
          let newProxy = IXArrayProxy.Create(k, container);
            newProxy[k] = selector.options;
            selector.options = newProxy;
          }
        }

        break;
    }
  });
}

Arrays

Arrays are something of a nightmare. Array functions, such as push, pop, and length, are actually vectored through the proxy's getter (as would any other function on a proxy'd object):

JavaScript
static ArrayChangeHandler = {
  get: function (obj, prop, receiver) {
  // return true for this special property, 
  // so we know that we're dealing with a ProxyArray object.
  if (prop == "_isProxy") {
    return true;
  }

  // Setup for push and pop, preserve state when the setter is called.
  // Very kludgy but I don't know of any other way to do this.
    if (prop == "push") {
    receiver._push = true;
  }

  if (prop == "pop") {
    receiver._pop = true;
  }

  if (prop == "length") {
    return obj[receiver._id].length;
  }

  return obj[prop];
},

Notice that a flag is being set as to whether the operation about to be performed, in the setter, is a push or pop! This information is used to determine how the array should be adjusted in the setter when the length is changed. Popping an array element, it turns out, merely changes the length of the array:

JavaScript
set: function (obj, prop, val, receiver) {
  // we're looking for this pattern:
  // "setting 0 for someList with value Learn Javascript"
  let id = receiver._id;
  console.log('setting ' + prop + ' for ' + id + ' with value ' + val);

  if (prop == "length" && receiver._pop) {
    let el = document.getElementById(id);
    let len = obj[id].length;

    for (let i = val; i < len; i++) {
      el.childNodes[val].remove();
      obj[id].pop();
    }

    receiver._pop = false;
  } else {

If the setter is not a pop, then it is either updating an existing item in the array:

JavaScript
// We might be setting an array item, or we might be doing a push, 
// in either case "prop" is the index value.
if (!isNaN(prop)) {
  let el = document.getElementById(id);
  switch (el.nodeName) {
    // TODO: "UL"!
    case "OL": {
    let n = Number(prop);
    let ol = el as HTMLOListElement;

    if (n < ol.childNodes.length && !receiver._push) {
      // We are replacing a node
      // innerText or innerHTML?
      (ol.childNodes[n] as HTMLLIElement).innerText = val;

or we are adding an item to the array:

JavaScript
} else {
  let li = document.createElement("li") as HTMLLIElement;
  let v = val;

  if (val._isTemplate) {
    let t = val as IXTemplate;
    // innerText or innerHTML?
    li.innerText = t.value;
    li.id = t.id;
    v = t.value;
  } else {
    li.innerText = val;
  }

  (el as HTMLOListElement).append(li);
  obj[id].push(v);
  receiver._push = false;
}

Lastly, the array property might be set to a whole new array:

JavaScript
} else if (val.constructor.name == "Array") {
  let el = document.getElementById(id);

  // TODO: remove all child elements?

  switch (el.nodeName) {
    case "SELECT":
      (val as IXOption[]).forEach(v => {
        let opt = document.createElement("option") as HTMLOptionElement;
        opt.innerText = v.text;
        opt.value = String(v.value);
        opt.disabled = v.disabled;
        opt.selected = v.selected;
        (el as HTMLSelectElement).append(opt);
      });
    break;

    case "OL":
    case "UL":
      (val as []).forEach(v => {
        let li = document.createElement("li") as HTMLLIElement;
        li.innerText = v;
        (el as HTMLOListElement).append(li);
      });
    break;
  }
}

IXEvent

IXEvent (and its helper, IXSubscriber) are wrappers to implement multicast events:

JavaScript
export class IXSubscriber {
  subscriber: (obj: any, oldVal: string, newVal: string) => void;

  constructor(subscriber: (obj: any, oldVal: string, newVal: string) => void) {
  this.subscriber = subscriber;
}

  Invoke(obj: any, oldVal: string, newVal: string): void {
    this.subscriber(obj, oldVal, newVal);
  }
}

import { IXSubscriber } from "./IXSubscriber"

export class IXEvent {
  subscribers: IXSubscriber[] = [];

  // We probably only usually want the new value, followed by the container, 
  // followed by the old value.
  Add(subscriber: (newVal: string, obj: any, oldVal: string) => void) : IXEvent {
    this.subscribers.push(new IXSubscriber(subscriber));

    return this;
  }

  Invoke(newVal: string, obj: any, oldVal: string): void {
    this.subscribers.forEach(s => s.Invoke(newVal, obj, oldVal));
  }
}

IXTemplate

This class is my lame attempt to provide for templates.

JavaScript
export class IXTemplate {
  public _isTemplate: boolean = true;

  public value?: string;
  public id?: string;

  public static Create(t: any): IXTemplate {
    let template = new IXTemplate();
    template.value = t.value;
    template.id = t.id;

    return template;
  }

  public SetValue(val: string): void {
    document.getElementById(this.id).innerText = val;
  }
}

This is dubious at best because it sets innerText rather than innerHtml, and I'm really not sure of the usefulness of it except that it's used in the integration tests.

Conclusion

Having come to the top of the other end of the U, I'm now reconsidering the entire implementation.

Do I need a Proxy?

By the time I was implementing an example of the combobox, and using this construct:

JavaScript
selector = new IXSelector();

It occurred to me, hmm, maybe all the class properties that map to DOM elements should be wrapped by an actual "helper." This would allow the wrapper to directly implement the DOM attributes and properties of an element which effectively eliminates the need for a Proxy! It would also eliminate the "make up my own syntax", like:

JavaScript
.Add({ bindFrom: "jane", attribute: "value" })

or:

JavaScript
mySpan = {
  attr: { title: "" }
};

The "initialization" process would merely iterate over the class properties (they still have to exist) and initialize the property with the DOM ID, thus the specific implementation can manipulate the DOM directly rather than through a proxy. And I'd still have Intellisense because the wrapper implemention would have the DOM attributes and properties I'd be touching.

Of course, this involves a ton of work - ideally the DOM for each element would have to be re-implemented, and that's just for native HTML elements. What about third party UI libraries? One approach would be to do this a piece at a time, as needed in the web apps that I'd be writing with this framework. Furthermore, I could derive such wrapper classes from the HTML.... interfaces that already exist, for example, HTMLButtonElement. That would work, but I also really like the beauty of:

JavaScript
nc.name = "Hello World!";

Both Proxy and DOM Wrapper

There's no reason the implementation cannot support both by inspecting the constructor name, as I'm already doing in the initialization process. This would therefore leave it up to the developer:

  1. For native types, a proxy would wrap basic behaviors.
  2. For wrapper types, a proxy would not be used, giving the developer more fine-grained control of the element.

This would also eliminate any arcane syntax that the developer would have to learn, as the wrapper would implement the HTML... interfaces that already exist and with which one would already be familiar.

Also, all that krufty code to handle array push, pop, and assignment would be a lot cleaner!

The Future Is Emerging

Therefore, I can only conclude that after having gone through the U process, I now know what the future will look like, at least for the framework that I want to use!

History

  • 5th July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionVery interesting Marc Pin
Pete O'Hanlon1-Sep-20 23:55
mvePete O'Hanlon1-Sep-20 23:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.