This is possible to achieve even without writing a JCR listener, and it’s nicely explained here.
Here’s the code snippet which shows how to do this:
<!-- the code snippet from a page component dialog --> <title jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/hidden" name="./jcr:title@ValueFrom" value="./firstName" defaultValue="./firstName"/>
When a new value is saved for firstName, Sling will update automatically jcr:title property thanks to @ValueFrom suffix. It’s also important to use granite/ui/components/foundation/form/hidden widget in order to work properly.