Using v-model with custom setters
Tags: vue, typescript
Published: 2023-08-09 21:36:36

In the [previous article](https://dev.to/razi91/vue-arrays-and-v-model-17e0) we created a proxy for array, so we could easily use `v-model` on items of a sorted and filtered array. This time we will write an actual Proxy for an object, so we could write our own setters and still use simple `v-model`, instead of splitting it into `:model-value` and `@update:modelValue`. Creating `computed`s for each field separately isn't a nice way to achieve it. Then how?

# Why?

Case: a picker for an item, but when the item is set, we want also to set it’s category in separate field (there can be picked category without selected item, that's why it's in separate field too).


![Relations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhu07uigi83gq0lm315x.png)


If we are setting an item, the category must match the item’s category. When we change category, we must ensure that the currently selected item matches the category or clear it. Let’s define our form:

```tsx
type EntryModel = {
  id: number | null;
  name: string;
  category: CategoryModel | null;
  item: ItemModel | null;
  // ...
};

type ItemModel = {
  id: number | null;
  name: string;
  category: CategoryModel;
  // ...
};

const form = ref<EntryModel>({
  //...
});
```

We might do it in several ways:

## Just leave validation

We could just leave a validation that checks if `item` has the category equal to the `category` in entry and force user to change it.

You should definitely have validation for that, just in case, but it’s nice to help user automate things, that can be done automatically.

## Using `watch`

We could simply just watch the `item` being changed and react on it:

```tsx
watch(() => form.value.item, (newItem) => {
  if (newItem == null) return;
  if (form.value.category?.id != newItem.category?.id) 
    form.value.category = newItem.category;
});

watch(() => form.value.category, (newCategory) => {
  if (form.value.item?.category.id == newCategory?.id) return;
  form.value.item = null;
});
```

Simple and… terrible. We will have to be sure we installed that watch only once. While it’s simple to write, its making a mess in the data flow which might end with endless loop. Notice that if we set here an item, the `item` watch will react and will probably change `category`, after which the watcher on it will be also called later.

Of course, if the data is valid, it should end here, but in more complex network of dependencies it might cause the infinite loop. It’s also hard to track what is changed and why. Also we need to remember, that `watch` will notice the change if `form.value.category` will change, even if the new instance will have the same `id`, we would need to listen to the `id` field then.

And what if we’d like to cancel the change, because some dependencies weren’t met, and we couldn’t check them while displaying the list of items to pick? It also might be important to know what user actually tried to set. We could also ask user that the action might have more consequences than changing that one value.

Also, we are watching instances here, not ids. If we assign the same category, but using different instance, the watcher will be called.

Just… no. This. Is. A. Mess.

## Using dedicated setter

as I mentioned before, we could write a dedicated setter, we have to assign `model-value` prop and event separately:

```html
<CategoryPicker
  :model-value="form.category"
  @update:modelValue="setCategory"
/>
<ItemPicker
  :model-value="form.item"
  @update:modelValue="setItem"
/>
```

And then have the setter written in our `setup`:

```tsx
const setCategory = (newCategory: CategoryModel) => {
  form.value.category = newCategory.newItem;
  if (form.value.item?.category?.id != newCategory?.id) {
    form.value.item = null;
  }
}

const setItem = (newItem: ItemModel) => {
  form.value.item = newItem.newItem;
  if (form.value.category.id != newItem.category.id) {
    // if it's required
    form.value.category = newItem.category;
  }
}
```

It will be executed in one tick, so no worries about unnecessary rerendering. It’s possible also to chain the setters, but we would have again to be careful to not get into infinite loop (actually, here we would get a stack overflow error):

```tsx
const setItem = (newItem: ItemModel) => {
  form.value.item = newItem.newItem;
  if (form.value.category.id != newItem.category.id) {
    // if it's required
    setCategory(newItem.category);
  }
}

```

We could also do it with one assignment (we will focus on one setter):

```tsx
const setItem = (item: ItemModel) => {
  const category = form.value.category.id == item.category.id 
    ? form.value.category 
    : item.category
  form.value = {
    ...form.value,
    item,
    category,
  };
}
```

This one might be preferred if `form` is already a `modelValue` and we want to emit a single event. That way we could also make it async, and before anything would be set:

```tsx
const setItem = async (item: ItemModel) => {
  const category = form.value.category.id == item.category.id 
    ? form.value.category 
    : item.category
  const res = await checkIfCanBeAssigned({ entry: form.value, item, category });
  if (!res) return;
  form.value = {
    ...form.value,
    item,
    category,
  };
}
```

We would need to block the inputs in the meantime, but we won’t focus on that now.

## Writing an actual proxy

Proxy is the core of Vue’s reactivity. It’s pretty everywhere, so why we shouldn’t write our own? We can’t write our own class with getters and setters, writing a loop with `Object.defineProperty` is a mess, while we have a much simpler solution.

Basically, Proxy is an object, that calls getters for every field we try to read and setter, when we try to set any. It allows us to track every read and write on the object, let’s look at simpliest Proxy (not TS compatible here):

```jsx
const proxy = new Proxy(form.value, {
  get (target, key) {
    return target[key];
  },
  set (target, key, value) {
    target[key] = value;
    return true;
  },
});
```

It simply… doesn’t do anything, just returns fields and sets… What’s interesting, we just wrote something that `reactive` does! With that, we can set fields with simple `proxy.category =` and that will just work fine! Why? We give it `form.value` as a target. We receive that object in getter and setter. Assuming that it was reactive (and `ref` is deeply reactive), then `target[key] = value` will be equal to `form.value[key] = value`. Of course it will fail if the `form.value` will change (by reassigning the instance by `form.value = {...}`). So we have to wrap it with a `computed`:

```tsx
const proxy = computed({
  get () {
    return new Proxy(form.value, {
      get (target, key) {
        return target[key];
      },
      set (target, key, value) {
        target[key] = value;
        return true;
      }
    });
  },
  set (v) {
    form.value = v;
  }
});
```

Now when `form.value` will be reassigned, the `computed` will return new Proxy instance targeting the new instance, but again, we need to get to the fields by proxy`.value`, but it shouldn’t be a problem. It’s still just a `Ref<T>`.

Okay, but what about our setters? We have just one here, right? Yeah, and we need to expand it. First, we need to form an interface. How setter should actually look like?

The most important is that we need to be able to set the field that we originally tried to set, but we found that we need to also be able to set other fields at the same time as well, or… none at all.

How about to make the setter a function, that returns a `Partial<T | null>` with the fields we want to set (or none, if `null`)? As arguments we would receive the object itself and value we want to set

```tsx
const setters = {
  item: async (item: ItemModel) => {
    const category = form.value.category.id == item.category.id 
      ? form.value.category 
      : item.category
    const res = await checkIfCanBeAssigned({ entry: form.value, item, category });
    if (!res) return null; //we set literally nothing
    return { // set just those two fields
      item,
      category,
    };
  },
  category: async (category: CategoryModel) => {
    const item = form.value.item?.category.id == category.id 
      ? form.value.item // keep current
      : null; // clear
    const res = await checkIfCanBeAssigned({ entry: form.value, item, category });
    if (!res) return null;
    return {
      item,
      category,
    };
  }
};
```

We can end the setter at any moment returning an object. What type should be `setters` of?

```tsx
type ProxySetters<T extends object> = Partial<{
  [K in keyof T]: (
    target: T,
    value: T[K]
  ) => Partial<T> | null | Promise<Partial<T> | null>;
}>;
```

It means, it’s a partial (we don’t need to cover every field) object of the given type `T` (which must be an object), where key is the name of a field and value is a function that returns `Partial<T>`, synchronous or asynchronous. Now we need to wrap it into a composable:

```tsx
export function toProxy<T extends object>(opt: {
  target: Ref<T>,
  setters: ProxySetters<T>,
}) {
  const proxy = computed({
    get () {
      return new Proxy(opt.target.value, {
        get (target: T, key: string) {
          return target[key as keyof T];
        },
        set (target: T, key: string, value: any) {
          const setterFn = opt.setters[key as keyof T];
          if (setterFn) {
            // custom setter available, call it
            const toSet = setterFn(target, value);
            // here we can debug the changes, maybe write your own devtools here?
            opt.target.value = {
              ...opt.target.value,
              ...toSet,
            };
          } else {
            // no custom setter, just apply value
            opt.target.value[key as keyof T] = value;
          }
          return true;
        }
      });
    },
    set (v) {
      opt.target.value = v;
    }
  });

  return {
    proxy
  };
}
```

We need to do some casting, because TS is sometimes a bit too overprotective, `key` is typed as `string`, because user can try to get any field (by either `.field` or `["field"])`, so we need to prepare for any situation), but… we don’t have to care about it here. Outside the Proxy object will be visible as `T` anyway. Moreover, `value` is typed as `any` and… we can’t protect it. TS won’t allow on it’s level to call setter with incompatible type anyway, so again, we don’t have to care about it here, just trust TS.

The problem is that setter can not be `async`, but… we can easily just return the `true` and wait for the result with `then`, like in not so good old times. Let’s focus on the setter in that case:

```tsx
if (key in opt.setters) {
  const toSet = opt.setters(target, value);
  if (toSet instanceof Promise) {
    toSet.then((toSet) => { // yes, overshadowing
      opt.target.value = {
        ...opt.target.value,
        ...toSet,
      };
    });
  } else {
    // just set, it's the result object
    opt.target.value = {
      ...opt.target.value,
      ...toSet,
    };
  }
  return true;
}

```

Or following the DRY rule:

```tsx
function setFields(toSet: Partial<T> | null) {
  if (toSet == null) return; // nothing to do
  // again... devtools? Logs?
  opt.target.value = {
    ...opt.target.value,
    ...toSet,
  };
  // or: Object.assign(opt.target.value, toSet);
  // if you really need to avoid overriding the object instance
};

if (key in opt.setters) {
  const toSet = opt.setters(target, value);
  if (toSet instanceof Promise) {
    toSet.then(setFields);
  } else {
    setFields(toSet);
  }
  return true;
}
```

Now it will work both synchronous and asynchronous! But as I mentioned earlier, the asynchronous setter could last a few seconds during which user could try to do something, it’s wise to lock the form for the time data being set.

Let’s set the rule: we can run only one setter at one time. No more. It would be also nice to know which field we are currently setting. Let’s add a variable:

```tsx
const currentlySetting = ref<keyof T>();
```

when it’s value is `undefined`, then no setter is running. Otherwise, it will contain the name of the setter. At the beginning of the Proxy’s setter, we need to put a guard:

```tsx
set (target: T, key: string, value: any) {
  if (currentlySetting.value) return false;
  // ...
}
```

We don’t even need to know what user is trying to set, we just don’t care, we shouldn’t allow any changes before the previous ones aren’t done. For synchronous there is no reason to set the variable, but calling the setter we don’t know yet if it will return `Promise` or not, therefore we need to modify our function:

```tsx
if (currentlySettting.value) return false;
const setterFn = opt.setters[key as keyof T];
if (setterFn) {
  currentlySetting.value = key;
  const toSet = setterFn(target, value);
  if (toSet instanceof Promise) {
    toSet
      .then(setFields)
      .finally(() => currentlySetting.value = undefined);
  } else {
    setFields(toSet);
    currentlySetting.value = undefined;
  }
  return true;
}
```

If the function will not be asynchronous `currentlySetting` will be set and unset in the same Vue’s tick, so there will be actually no change at all. Even if we would put a `watch` on it, we won’t register any change. But if the setter will be asynchronous, setting the `undefined` will happen in different tick and the change will be noticed by computed, even if the setter will end without calling any async task.

And we might also want a helper `isProcessing`, that will return if any field is being setted asynchronically. As I mentioned, if the `currentlySetting` will be set to anything and back to `undefined` in the same tick, it won’t change to true at all.

```tsx
return {
  proxy,
  currentlySettting, //if we need to know exactly which field
  isProcessing: computed(() => currentlySetting.value != undefined),
};
```

Now in our form we can lock the fields:

```html
<ItemPicker
  v-model="proxy.item"
  :loading="currentlySetting == 'item'"
  :disabled="isProcessing"
/>
```

Or just put an overlay on the whole form or even screen. The best thing is that we don’t even need to care about it in the setters, it’s wrapped inside.

Let’s summarize the code

```tsx
export type ProxySetters<T extends object> = Partial<{
  [K in keyof T]: (target: T, value: T[K]) => Partial<T> | Promise<Partial<T>>
}>;

export function toProxy<T extends object>(opt: {
  target: Ref<T>;
  setters: ProxySetters<T>;
}) {
  // <(string & {})> -- a trick for "any string, but still suggest the keys"
  const currentlySetting = ref<keyof T | (string & {})>();

  function setFields(toSet: Partial<T>) {
    opt.target.value = {
      ...opt.target.value,
      ...toSet,
    };
  }

  const proxy = computed({
    get() {
      return new Proxy(opt.target.value, {
        get(target: T, key: string) {
          return target[key as keyof T];
        },
        set(target: T, key: string, value: any) {
          if (currentlySetting.value) return false;
          const setterFn = opt.setters[key as keyof T];
          if (setterFn) {
            currentlySetting.value = key;
            const toSet = setterFn(target, value);
            if (toSet instanceof Promise) {
              toSet
                .then(setFields)
                .finally(() => (currentlySetting.value = undefined));
            } else {
              setFields(toSet);
              currentlySetting.value = undefined;
            }
          } else {
            opt.target.value[key as keyof T] = value;
          }
          return true;
        },
      });
    },
    set(v) {
      opt.target.value = v;
    },
  });

  return {
    proxy,
    currentlySetting,
    isProcessing: computed(() => currentlySetting.value != undefined),
  };
}
```

Why and how it would work?

How I mentioned before, when the `proxy.value` is accessed, it returns a new Proxy. At this level it acts list like any `computed` returning an object. The returned object will be reactive… in some way, but you shouldn’t see any difference. There is only one getter and one setter here, but inside it returns the field from reactive object: `proxy.value.name` is equivalent to calling a function that will return the same thing. Therefore, the dependency will be mounted.

The setter… it depends on the strategy. In case of `opt.target.value[key as keyof T] = value;` there is no creating any new instance, but the reactivity should notify all this field’s listeners that it changed its value.

If the `setFields` will be called, there will be new instance, then the `computed` will be outdated and will return new value. It will cause rerender of every component that uses that instance directly. It shouldn’t be any problem anyway.

Also it is perfectly safe to use alongside with the original `target`, omiting the `Proxy` mechanism, if not required (however, I would avoid that, it’s not why we implement that thing, to accidentally assign something omiting the setter!)

You can expand the `opt` argument with another options to let developer choose the strategy on specific places.

## Async setters

They are a bit tricky…Let’s take a case where we put a setter on a text field, that checks if every character is lowercase, let’s have a `ref`:

```tsx
const item = ref({
  id: 1,
  name: "lorem",
  description: "ipsum",
});
```

and make a proxy:

```tsx
const { proxy, isProcessing } = toProxy({
  target: item,
  setters: {
    name: async (obj, val) => {
      if (val == val.toLowerCase()) {
        return { name: val };
      }
      // a function that resolves after 500ms
      await delay(500);
      return {
        name: val.toLowerCase()
      }
    },
  },
});
```

The problem is that `isProcessing` will be set to `true`, because we have here a few Vue’s ticks. The `name` setter here is itself async, therefore it will return `Promise`. It means the `currentlySetting` won’t be set back to `undefined` in the same tick. It means the template will be in the meantime rerendered and we might lose the focus on the input element.

It shouldn’t be a problem for dropdown pickers, but for text inputs it will be critical. We should make an old-fashion function that just returns a Promise:

```tsx
const { proxy, isProcessing } = toProxy({
  target: item,
  setters: {
    name: (obj, val) => {
      if (val == val.toLowerCase()) {
        // returned synchronically, in the same tick
        return { name: val };
      }
      // return a Promise
      return delay(500).then(() => ({
        name: val.toLowerCase(),
      }));
    },
  },
});
```

Now if everything will be fine, no uppercase letter would appear, it will return a synchronous object. Otherwise, it will return a Promise and will have to wait for it to end.

## Dialogs and more complex controllers

Using the async setters you can ask user for something, maybe user wasn’t aware of the consequences of flipping single switch? Let’s say we have a form, where we have a switch and a list of elements. If the switch goes off, it will remove all elements. We then just can’t use the `watch` method here, because we can’t allow the change to happen, if user won’t allow that! It’s possible to solve with a simple async setter (using Quasar’s Dialog plugin):

```tsx
setters: {
  enabled: (obj, enabled) => {
    if (enabled || obj.items.length == 0) return { enabled };
    return new Promise((resolve) => {
      Dialog.create({
        title: 'Confirm',
        message: 'This action will remove all items in list',
        cancel: true,
        persistent: true
      }).onOk(() => {
        resolve({ enabled, items: [] })
      }).onCancel(() => {
        resolve(null);
      });
    });
  }
}
```

In this case it either updates the `enabled` field, if it’s enabling or the list is empty, or asks users if they are aware of the consequences first.

While you have to provide `setters` in the exact form, it doesn’t need to be that simple. You can write complex controllers:

```tsx
// global utility
function ask<T extends object>(
  title: string,
  message: string,
  onOk: () => Partial<T>
): Promise<Partial<T> | null> {
  return new Promise((resolve) => {
    Dialog.create({
      title,
      message,
      cancel: true,
      persistent: true,
    })
      .onOk(() => {
        resolve(onOk());
      })
      .onCancel(() => resolve(null));
  });
}

function useItemForm(item: Ref<Item>) {
  // loose utilities and setters
  function setEnabled(enabled: boolean) {
    return { enabled };
  }

  // can be defined separately
  const setters = {
    enabled: (obj, enabled) => {
      if (enabled || obj.items.length == 0) return setEnabled(enabled);
      return ask<Item>(
        "Confirm",
        "This action will remove all items in list",
        () => setEnabled(enabled)
      );
    },
  } satisfies ProxySetters<Item>;
  
  return toProxy({
    target: item,
    setters,
  });
}
```

We can easily test it separately from Vue components, making the testing much faster and easier. Notice also that setters have no dependencies on Vue, which mean you can share this code with your Node.js backend!

## Chaining setters

We can still chain the setters here. Of course, there is a risk of making infinite loop here, so be careful to avoid that. Maybe if we set the `category`, we just know we need to clean the `item`. It’s still not wise to allow to make circular dependency between setters, but you can make some easier. Having setters declared separately, like above (with `satisfies`, not `setters: ProxySetters<T>`), we can just call another setter with changed object:

```tsx
 const setters = {
  // ... other setters
  item: async (obj, item) => {
    const setCategory = item?.category.id != obj.category.id 
      ? await setters.category(
          {...obj, item}, // we can fake the original object with overwritten values
          item?.category ?? null // provide a value to set
        ) 
      : {}; // no need to change anything
    return {
      ...setCategory, //  could also change the `item`, or literally nothing
      item, // but we set it here anyway
    };
  },
} satisfies ProxySetter<Type>;
```

We might not know if changing category shouldn’t change also something else (or it’s logic is more complicated and we don’t want to repeat it). That way, whatever else it changes, it will change. Changing category could require asking user for confirmation, se we should make it async (it’s not a text field, so it shouldn’t be a problem).

And still, in this case, I wouldn’t recommend calling `item` setter from `category` setter, as it might cause stack overflow. It would be better here to avoid calling other setters, if changing is not required.

On the way, we can fake the original item, simulating some changes being done already. We might call two setters one after another, and pass to the second one the modified object with applied changes from the first setter.

No actual change on original item the `Ref` is referring to will happen, until the original setter wouldn’t return the object with changes to perform. We can also perform a validation before saving the changes on the original object and reject them, if validator would find any problem.

## Two strategies at the same time

What if we want to use both strategies at the same time? Proxy is a magical object in which you can program both getters and setters for any key, it is just a virtual interface for an actual object. We could easily manipulate it, by adding another set of fields, prefixed with `$`, assuming there aren’t any already, it should work fine. We will need to add a cast:

```tsx
return new Proxy(opt.target.value, {
  /// ...
}) as T & {
  [K in keyof T as `$${string & K}`]: T[K];
};
```

Define the actual setter strategies:

```tsx
function setFields(toSet: Partial<T> | null) {
  if (toSet == null) return;
  Object.assign(opt.target.value, toSet);
}

function setByReassign(toSet: Partial<T> | null) {
  if (toSet == null) return;
  opt.target.value = {
    ...opt.target.value,
    ...toSet,
  };
}

```

We will need to modify our getter too:

```tsx
get(target: T, key: string) {
  if (key.toString().startsWith('$')) {
    return target[key.substring(1) as keyof T];
  }
  return target[key as keyof T];
},
```

and about setter, we need to first decide which strategy should be used:

```tsx
set(target: T, _key: string, value: any) {
  const reassign = _key.startsWith('$');
  const setter = reassign ? setByReassign : setFields;
  const key = reassign ? _key.substring(1): _key;
  // ...
```

And later replace `setFields` with `setter`.

Now if the original object had field `name`, it will have also a field called `$name`, which getter will return just `name`, and setter will update the object by replacing it’s instance.

But seriously… it would be better to just return 2 types of proxies. This is just an example of what proxies are able to do.

## Track fields that changed

Another nice feature we can implement here, is tracking the fields that changed. Having a function being called on every set, we can track fields that changed and mark them on form or even allow to undo the change.

We would need to store the original object first, with `structuredClone`, then in setters `setFields` and `setByReassign` we would have to call this:

```tsx
const original = structuredClone(opt.target.value);
const changedFields = ref(new Set<string>());

// this one we will call in setters:
function setChanged(toSet: Partial<T>) {
  for (const [key, value] of Object.entries(toSet)) {
    if (value != original[key]) changedFields.add(key);
    // if it's the same as original, unmark that it changed
    else changedFields.delete(key);
  }
} 

// this one we will just expose:
function restoreField(key: keyof T & string) {
  if (!changedFields.value.has(key)) return;
  proxy.value[key] = original[key]; //call the setter
}
```

The new setter will update the `changedFields` status. That way we could inform user that nothing changed in the form.

Presented listener won’t work fine, if the component will be recreated or created twice and the value change will happen outside the proxy. It also doesn’t track changes in nested objects.

## Ignore field with Symbol

Sometimes writting setters, we will have to either set other field or not. We could just return a partial without that field, but sometimes it would mean returning 2 different objects, splitting the code into two branches.

Other idea is to return the field as `undefined` value, which would mean “no fiels”, but what if in that object `undefined` would be a valid value, along `null` and any other? JS offers an unique objects that are their own types: `Symbol`s. Let’s create a symbol called `IgnoreField`:

```tsx
export const IgnoreField: unique symbol = Symbol("ignore");
```

Now if we would want to ignore the field, we would just return this symbol at the field. We will need to redefine our types, replace `Partial<T>` (in `ProxySetters`) with:

```tsx
export type ProxyPartialSetter<T extends object> = {
  [K in keyof T]?: T[K] | typeof IgnoreField;
};
```

It means: object, that is essentially a Partial of T, but allows to assign `FieldIgnore` at any field. Next, we will need to write a function that will clear the returned object from fields containing that setter:

```tsx
function clearupSetters<T extends Object>(toSet: ProxyPartialSetter<T>): Partial<T> {
  const copy = {...toSet};
  for(const key of Object.keys(copy)) {
    if (copy[key as keyof T] == IgnoreField) {
      delete copy[key as keyof T];
    }
  }
  return copy as Partial<T>;
}
```

Sure, functional purists will hate me now, so now a functional approach:

```tsx
function clearupSetters<T extends Object>(
  toSet: ProxyPartialSetter<T>
): Partial<T> {
  return Object.fromEntries(
    Object.entries(toSet).filter(([key, value]) => value != IgnoreField)
  ) as Partial<T>;
}
```

It’s slower (at the time of Chrome 115), but if you really put “readibility” of a 5-lines function above performance, then it’s up to you. Also, the first variant without copying the object is even faster, so if you don’t care what happens to this object and don’t want to lose additional nanoseconds, feel free to avoid copying it before removing fields.

Next, we need to modify our setters:

```tsx
function setFields(toSet: ProxyPartialSetter<T> | null) {
  if (toSet == null) return;
  Object.assign(opt.target.value, clearupSetters(toSet));
}
```

And that’s it, now we can write setters like:

```tsx
const setters = {
  item: (obj, item) => ({
    item,
    category: item?.category?.id == obj.category?.id 
      ? IgnoreField // nothing will be changed
      : item?.category
  })
}
```

## Conclusion

Just as wrapping the arrays, we can wrap an object in a `Proxy` and write our own setters, adding some more functionalities, like locking whole form when asynchronous setters are working. The performance impact of this solution is negligible, especially if we won’t use full instance overwrite and we won't use setters for text fields.

With this solution, we can easily handle async setters without the need to implement it in each setter. All that without having to care about it in the template.

This is just the beginning. We could modify it to return much more complex structures, nested proxies with their own setters etc.