Router Component

Router Component is a special type of content that can be loaded by Router when we specify route content using component or componentUrl properties.

It should help to better structure our apps, keep things in appropriate place, and make many things quicker and in a more clear and comfortable way.

Component Function

Component is a function that receives props and context and should return render function.

Component render function should return Tagged template literal with component HTML content.

For example:

const MyComponent = (props, context) => {
  // some component logic
  let value = 'foo';

  // return render function
  return () => context.$h`
    <div class="page">
      <p>Value is ${value}</p>
    </div>
  `;
}

Component Template

As mentioned above component render function should return Tagged template literal with component HTML content. It has few important things to pay attention to.

All self-closing tags must be closed!. If you will have not closed self-closing tags like <br>, <img src="">, <input ...>, then compiler will throw an error.

All empty elements can be self-closed:

<div class="my-div"></div>

<!-- also valid as -->
<div class="my-div" />

Component Props

First argument that receives component function is props. This object will contain all props that you will pass in navigate method, and all route parameters.

For example if we have the following route:

{
  path: '/blog/:id',
  component: MyComponent
}

And when we navigate to route via /blog/34/ URL then it will have props.id equal to '34'.

And also when we navigate to component using API like so:

router.navigate('/blog/34/', {
  props: {
    foo: 'bar'
  }
})

Then props will be the following object: { id: '34', foo: 'bar' }

Also props will contain properties passed to custom component as attributes. If custom component has such attributes:

<my-component foo="bar" id="25" user=${{name: 'John'}} number=${30}></my-component>

then $props will be:

{
  foo: 'bar',
  id: '25',
  user: {
    name: 'John'
  },
  number: 30
}

Component Context

context object contains a lot of useful helpers:

PropertyDescription
$h

Special Tagged template literal which is must be used to wrap component render function result and all HTML entries inside:

const MyComponent = (props, { $h }) => {
  let list = ['item 1', 'item 2'];

  return () => $h`
    <div class="page">
      <ul>
        ${list.map((item) => $h`
          <li>${item}</li>
        `)}
      </ul>
    </div>
  `
}
$el

Object where .value property contains Dom7 instance with component HTML element.

$el.value will be available only after component has been mounted (or in any page events like pageInit).

const MyComponent = (props, { $el, $onMounted }) => {
  $onMounted(() => {
    $el.value.find('p').addClass('red')
  })
  // ...
}
$

Dom7 library:

const MyComponent = (props, { $, $onMounted }) => {
  $onMounted(() => {
    $('p').text('hello world')
  })
  // ...
}
$f7

Framework7 app instance

$f7.dialog.alert('Hello world!')
$store

Store instance. Check Store documentation for more details and examples.

$f7routeCurrent route. Contains object with route query, hash, params, path and url
$f7router

Related router instance

$f7router.back(); //navigate back
$theme

Object with md, ios and aurora boolean properties which indicating current theme. For example:

if ($theme.ios) { /* do something when iOS theme is active */ }
if ($theme.md) { /* do something when MD theme is active */ }
if ($theme.aurora) { /* do something when Aurora theme is active */ }
$update(callback)

This method tells that this component and its children need to be re-rendered with the updated state

const MyComponent = (props, { $update, $h }) => {
  // initial state
  let value = 'foo';

  const updateValue = () => {
    // update local state
    value = 'foo2';
    // call $update method
    $update();
  }

  return () => $h`
    <div class="page">
      <p>Value is ${value}</p>
      <button @click=${updateValue}>Update Value</button>
    </div>
  `;
}

It is not guaranteed that the DOM changes are applied immediately, so if you rely on DOM (e.g. need to get HTML content or attribute values after state changed) then pass callback function as argument.

$tick(callback)

You can also use this method if you rely on DOM and need to be sure that component state and DOM updated after calling $update() methods.

Passed callback will be executed on DOM update.

This method returns Promise that will also be resolved on DOM update.

So you can use it as this:

$update();

$tick(function () {
  console.log('DOM and state updated');
});

// Or as Promise
$tick().then(() => {
  console.log('DOM and state updated');
})

// Or in async function/method as:
await $tick();
console.log('DOM and state updated');
$f7ready(callback)

This method need to be used only when you use Main App Component to make sure to call Framework7 APIs when app initialized.

const AppComponent = (props, { $f7, $f7ready }) => {
  $f7ready(() => {
    // now it is safe to call Framework7 APIs
    $f7.dialog.alert('Hello!');
  });

  // ...
}
Events
$on

Function to attach DOM events handlers to component root element

const MyComponent = (props, { $on }) => {
  // attach 'pageInit' event handler
  $on('pageInit', (e, page) => {
    console.log(page.name)
  });
  // ...
}

Such event handlers will be automatically detached when component destroyed

$once

Function to attach DOM events handlers to component root element. Same as $on but such handlers will be executed only once.

$emit(event, data)

Function to emit custom DOM events in re-usable custom components:

const MyComponent = (props, { $emit }) => {
  // emits custom event
  $emit('myevent')
  // ...
}

And in other parent component:

<my-component @myevent=${doSomething} />
Lifecycle Hooks
$onBeforeMountCalled right before component will be added to DOM
$onMounted

Called right after component was be added to DOM

const MyComponent = (props, { $onMounted }) => {
  // do something when component mounted
  $onMounted(() => {
    console.log('component mounted')
  });
  // ...
}
$onBeforeUpdateCalled right after component before VDOM will be patched/updated
$onUpdatedCalled right after component VDOM has been patched/updated
$onBeforeUnmountCalled right before component will be unmounted (detached from the DOM)
$onUnmountedCalled when component unmounted and destroyed

So the example route with page component may look like:

routes = [
  // ...
  {
    path: '/some-page/',
    // Component
    component: (props, { $h, $f7, $on }) => {
      const title = 'Component Page';
      const names = ['John', 'Vladimir', 'Timo'];

      const openAlert = () => {
        $f7.dialog.alert('Hello world!');
      }

      $on('pageInit', (e, page) => {
        // do something on page init
      });
      $on('pageAfterOut', (e, page) => {
        // page has left the view
      });

      return () => $h`
        <div class="page">
          <div class="navbar">
            <div class="navbar-bg"></div>
            <div class="navbar-inner">
              <div class="title">${title}</div>
            </div>
          </div>
          <div class="page-content">
            <a @click=${openAlert} class="red-link">Open Alert</a>
            <div class="list simple-list">
              <ul>
                ${names.map((name) => $h`
                  <li>${name}</li>
                `)}
              </ul>
            </div>
          </div>
        </div>
      `;
    },
  },
  // ...
]

Component Page Events

Component page events handlers can be passed in $on component event handler. They are usual DOM Page Events. Because they are DOM events, they accept event as first agrument, and Page Data as second argument. There only difference with usual DOM events is that event handler name must be specified in camelCase format (page:init -> pageInit):

const MyComponent = (props, { $on }) => {
  $on('pageMounted', (e, page) => {
    console.log('page mounted');
  });
  $on('pageInit', (e, page) => {
    console.log('page init');
  });
  $on('pageBeforeIn', (e, page) => {
    console.log('page before in');
  });
  $on('pageAfterIn', (e, page) => {
    console.log('page after in');
  });
  $on('pageBeforeOut', (e, page) => {
    console.log('page before out');
  });
  $on('pageAfterOut', (e, page) => {
    console.log('page after out');
  });
  $on('pageBeforeUnmount', (e, page) => {
    console.log('page before unmount');
  });
  $on('pageBeforeRemove', (e, page) => {
    console.log('page before remove');
  });
}

DOM Events Handling

Note that additional @ attribute in component template. It is a shorthand method to assign event listener to the specified element. Specified event handler will be searched in component scope.

Such event handler attribute value must be a function:

const MyComponent = (props, { $h, $update }) => {
  let value = 10;
  const addValue = (number) => {
    value += number;
    $update();
  }
  const onClick = () => {
    console.log('click');
  }

  return () => $h`
    <div class="page">
      <!-- pass function to attribute -->
      <button @click=${onClick}>Button</button>

      <!-- also work -->
      <button @click=${() => onClick()}>Button</button>

      <!-- will not work, attribute value "onClick" is just a string -->
      <button @click="onClick">Button</button>

      <!-- passing dynamic data will work as expected -->
      <button @click=${() => addValue(15)}>Button</button>
    </div>
  `
}

Event handlers are processed only on initial rendering, or for elements patched with VDOM. If you add such element to DOM manually it won't work!

const MyComponent = (props, { $h, $on }) => {
  const onClick = () => {
    console.log('click');
  }

  $on('pageInit', (e, page) => {
    // this won't work
    page.$el.append('<a @click="onClick">Link</a>');
  });

  return () => $h`
    <div class="page">
    </div>
  `
}

Component Root Element

Component template or render function must return only single HTML element. And it must be an element that is supported by router:

  • If you load pages as router component then router component must return Page element:

    <template>
      <div class="page">
        ...
      </div>
    </template>

  • If you load modal (Routable Modals) as router component then router component must return that modal element:

    <template>
      <div class="popup">
        ...
      </div>
    </template>

  • If you load panel (Routable Panels) as router component then router component must return Panel element:

    <template>
      <div class="panel panel-left panel-cover">
        ...
      </div>
    </template>

  • If you load tab content (Routable Tabs) as router component then router component must return Tab's child element that will be inserted inside of routable Tab:

    <template>
      <div class="some-element">
        ...
      </div>
    </template>
    

Single File Component

It is not very comfortable to specify all component routes under same routes array, especially if we have a lot of such routes. This is why we can use componentUrl instead and put component into single file:

routes = [
  ...
  {
    path: '/some-page/',
    componentUrl: './some-page.f7.html',
  },
  ..
];

And in some-page.f7.html:

<!-- component template, uses same tagged template literals -->
<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">${title}</div>
      </div>
    </div>
    <div class="page-content">
      <a @click=${openAlert}>Open Alert</a>
      <div class="list simple-list">
        <ul>
          ${names.map((name) => $h`
            <li>${name}</li>
          `)}
        </ul>
      </div>
    </div>
  </div>
</template>
<!-- component styles -->
<style>
  .red-link {
    color: red;
  }
</style>
<!-- rest of component logic -->
<script>
  // script must return/export component function
  export default (props, { $f7, $on }) => {
    const title = 'Component Page';
    const names = ['John', 'Vladimir', 'Timo'];

    const openAlert = () => {
      $f7.dialog.alert('Hello world!');
    }

    $on('pageInit', () => {
      // do something on page init
    });
    $on('pageAfterOut', () => {
      // page has left the view
    });

    // component function must return render function
    return $render;
  }
</script>

Well, now it is much cleaner. The <template> and <style> tags will be automatically converted to the same properties of exported component.

It is mandatory to have return $render in the end of the component function, as it will be replaced by parser with content of the <template> tag.

Usage With Webpack

There is a special framework7-loader plugin for Webpack that allows to bundle Single-File Components into main bundle and not to use XHR (e.g. componentUrl) to load and parse component files each time.

This loader parses Single-File component's file and transforms it to plain JS object during bundling process. So, potentially, it can increase app performance because there won't be runtime parsing and compilation.

When this loader is configured, we need to store Single-File components in .f7.html files and use export default for component export:

<template>
  <div class="page">
    ...
  </div>
</template>
<script>
  export default () => {
    let foo = 'bar';

    const doThis = () => {
      // ...
    }

    return $render;
  }
</script>

It also possible to import required dependencies and styles:

<template>
  <div class="page">
    ...
  </div>
</template>
<script>
  import './path/to/some-styles.css';
  import utils from './path/to/utils.js';

  export default () => {
    let foo = 'bar';
    let now = utils.now();

    const doThis = () => {
      // ...
    }

    return $render;
  }
</script>

And then we can import it and add to routes:

// routes.js

import NewsPage from './path/to/news.f7.html';
import ServicesPage from './path/to/services.f7.html';

export default [
  {
    path: '/news/',
    component: NewsPage,
  },
  {
    path: '/services/',
    component: ServicesPage,
  }
]

JSX

Template literals doesn't have good syntax highlighting inside of HTML documents. But when using with webpack it is also possible to write components in JSX syntax.

To make it work, we need to store components in .f7.js files and write them using JSX:

export default (props, { $update }) => {
  let value = 10;
  const items = ['Item 1', 'Item 2'];

  const addValue = (number) => {
    value += number;
    $update();
  }

  //- render function should returns JSX
  return () => (
    <div class="page">
      <p>The value is {value}</p>
      <p>
        {/* JSX doesn't support @ in attribute name so event handlers should start from "on" */}
        <button onClick={() => addValue(10)}>Add Value</button>
      </p>
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  )
}

And import them in the same way in routes.js:

import NewsPage from './path/to/news.f7.js';
import ServicesPage from './path/to/services.f7.js';

export default [
  {
    path: '/news/',
    component: NewsPage,
  },
  {
    path: '/services/',
    component: ServicesPage,
  }
]

Virtual DOM

Virtual DOM and all VDOM related features available from Framework7 version 3.1.0.

The virtual DOM (VDOM) is a programming concept where an ideal, or "virtual", representation of a UI is kept in memory and synced with the "real" DOM. It allows us to express our application's view as a function of its state.

VDOM library called Snabbdom because it is extremely lightweight, fast and fits great for Framework7 environment.

So how does Framework7 router component VDOM rendering works? Component template is converted to VDOM instead of directly inserting to DOM. Later, when component state changes, it creates new VDOM and compares it with previous VDOM. And based on that diff it patches real DOM by changing only elements and attributes that need to be changed. And all this happens automatically!

Let's look at that user profile component example that will auto update layout when we request user data:

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">Profile</div>
      </div>
    </div>
    <div class="page-content">
      ${user && $h`
        <!-- Show user list when it is loaded -->
        <div class="list simple-list">
          <ul>
            <li>First Name: ${user.firstName}</li>
            <li>Last Name: ${user.lastName}</li>
            <li>Age: ${user.age}</li>
          </ul>
        </div>
      `}
      ${!user && $h`
        <!-- Otherwise show preloader -->
        <div class="block block-strong text-align-center">
          <div class="preloader"></div>
        </div>
      `}
    </div>
  </div>
</template>
<script>
  export default (props, { $on, $f7, $update }) => {
    // empty initial user data
    let user = null;

    $on('pageInit', () => {
      // request user data on page init
      $f7.request.get('https://api.website.com/get-user-profile').then((res) => {
        // update user with new data
        user = res.data;
        // trigger re-render
        $update();
      });
    })

    return $render;
  }
</script>

Note, that direct assignment to component state won't trigger layout update. Use $update whenever you need to update component layout!

Keys in Lists & Auto-Init Components

When VDOM is updating a list of elements, by default it uses an "in-place patch" strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, it will patch each element in-place and make sure it reflects what should be rendered at that particular index.

This default mode is efficient, but only suitable when your render output does not rely on child component state or temporary DOM state (e.g. form input values).

To give VDOM a hint so that it can track each node's identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item.

When rendering lists, an ideal value for key would be the unique id of each item:

<template>
  ...
  <ul>
    ${items.map((item) => $h`
      <li key=${item.id}>...</li>
    `)}
  </ul>
  ...
</template>
<script>
  export default () => {
    const items = [
      {
        id: 1,
        title: 'Item A'
      },
      {
        id: 2,
        title: 'Item B'
      },
    ];

    return $render;
  }
</script>

Same with auto-initialized components like Range Slider, Gauge and others that should be automatically initialized (if they have range-slider-init, gauge-init) when they added to DOM, and automatically destroyed when they removed from DOM. So such elements must be also indentified with unique keys.

<template>
  <div class="page">
    ...
    <div class="page-content">
      ${gaugeVisible && $h`
        <!-- must have unique key -->
        <div key="gauge" class="gauge gauge-init" data-type="circle"
          data-value="0.60"
          data-value-text="60%"
          data-value-text-color="#ff9800"
          data-border-color="#ff9800"
        ></div>
      `}
      ...
      <a href="#" class="button" @click=${showGauge}>Show Gauge</a>
    </div>
  </div>
</template>
<script>
  export default (props, { $update }) => {
    let gaugeVisible = false;

    const showGauge = () => {
      gaugeVisible = true;
      $update();
    }

    return $render;
  }
</script>
  • Note that key attribute must be unique accross single component.
  • If key attribute was not specified and element has an id attribute, then id attribute will be used as virtual node unique key.

innerHTML

If we need to insert HTML string (for example, received from API endpoint), we need to use special innerHTML element prop/attribute:

<template>
  <div class="page">
    ...
    <div class="block" innerHTML=${customHTML}></div>
  </div>
</template>
<script>
  export default (props) => {
    const customHTML = '<p>Hello <b>World!</b></p>';

    return $render;
  }
</script>

Using innerHTML on element will override all its children.

HTML content passed in innerHTML is just a string and, for example, component event handlers (like @click attribute) won't work.

Main App Component

It is possible to make whole app layout as a component.

Note that due to VDOM implementation it is highly recommended to add unique id or key attribute to every auto initialized View (View with view-init class):

To enable it, first, we should keep app root element empty in index.html:

<body>
  <!-- empty app root element -->
  <div id="app"></div>
</body>

Then we need to create main app component, for example, Single File Component using webpack:

<!-- app.f7.html -->
<template>
  <div id="app">
    ${loggedIn.value && $h`
      <div class="panel panel-left panel-reveal panel-init">
        <!-- every View has unique ID attribute -->
        <div class="view view-init" id="view-panel" data-url="/panel/"></div>
      </div>
      <div class="view view-main view-init" id="view-main" data-url="/"></div>
    `}
    ${!loggedIn.value && $h`
      <div class="login-screen modal-in">
        <div class="view view-init" id="view-auth" data-url="/auth/"></div>
      </div>
    `}
  </div>
</template>
<script>
  export default (props, { $store }) => {
    const loggedIn = $store.getters.loggedIn;

    return $render;
  }
</script>

Finally, when we init Framework7, we need to specify app component on init:

// import main app component
import App from './path/to/app.f7.html';

var app = new Framework7({
  // specify main app component
  component: App,
})

Or, if we don't use webpack, we can also load it via XHR:

var app = new Framework7({
  // load main app component
  componentUrl: './path/to/app.f7.html',
})

Also note that main app component will be mounted (added to DOM) BEFORE app initialization process finished. So if you need to call Framework7 APIs immediately, use $f7ready callback:

<template>
  <div id="app">
    ...
  </div>
</template>
<script>
  export default (props, { $f7ready, $f7 }) => {
    $f7ready(() => {
      // now it is safe to call Framework7 APIs
      $f7.dialog.alert('Hello!');
    })
  }
</script>

Custom Components

Register Components

It is possible to create custom reusable components. We need to do it BEFORE Framework7 initialization with the following method:

Framework7.registerComponent(tagName, component)- register custom component

  • tagName - string. Component tag name, e.g. my-component (will be used as <my-component>).

    Custom component tag name must contain a hyphen/dash character "-"

  • component - object or class. Component function

Note, at the moment, it is possible to use custom components only in router components (components loaded by router).

Framework7.registerComponent(
  // component name
  'my-list-item',

  // component function
  (props, { $h }) => {
    let foo = 'bar';

    return () => $h`
      <li class="item-content" id="${props.id}">...</li>
    `
  }
)

And use it in other components like:

<div class="list">
  <ul>
    <my-list-item id="item-1"></my-list-item>
  </ul>
</div>

Note, that attributes passed to custom component element available in component props.

Local Components

It is possible to create local custom components in components:

<template>
  <ul>
    <!-- use tag names as variables -->
    <${ListItem} title="Item 1" />
    <${ListItem} title="Item 2" />
    <${ListItem} title="Item 3" />
  </ul>
</template>
<script>
  // create local component
  const ListItem = (props, { $h }) => {
    return () => $h`<li>${props.title}</li>`;
  }

  // export main component
  export default () => {
    return $render;
  }
</script>

Or they can be imported:

<template>
  <ul>
    <!-- use tag names as variables -->
    <${ListItem} title="Item 1" />
    <${ListItem} title="Item 2" />
    <${ListItem} title="Item 3" />
  </ul>
</template>
<script>
  // import component
  import ListItem from 'path/to/list-item.f7.html';

  // export main component
  export default () => {
    return $render;
  }
</script>

With JSX:

const ListItem = (props) => {
  return (
    <li>{props.title}</li>
  )
}
/* or
import ListItem from 'path/to/list-item.f7.js'
*/

export default () => {
  return () => (
    <ul>
      <ListItem title="Item 1" />
      <ListItem title="Item 2" />
      <ListItem title="Item 3" />
    </ul>
  )
}

In JSX, it can be created inside of the main component:

export default () => {

  const ListItem = (props) => {
    return (
      <li>{props.title}</li>
    )
  }

  return () => (
    <ul>
      <ListItem title="Item 1" />
      <ListItem title="Item 2" />
      <ListItem title="Item 3" />
    </ul>
  )
}

Events

You can assign DOM events for custom component in templates with same @{event} syntax. Event handler will be actually attached to custom component root element.

<template>
  <div class="page">
    ...
    <my-button @click="onClick">Click Me</my-button>
  </div>
</template>
<script>
  return {
    // ...
    methods: {
      onClick: function(e) {
        console.log('clicked');
      }
    },
    // ...
  }
</script>

Slots

If we need to pass children elements (or text) to custom component we need to use slots. Slots implementation here is similar to Web Components slots.

With slot tag we specify where component children should be placed. For example my-button component template:

<a class="button button-fill">
  <slot></slot>
</a>

Can be used then like this:

<my-button>Click Me</my-button>

To specify slot default value (when no children passed), we just put it inside <slot> tag:

<a class="button button-fill">
  <slot>Default Button Text</slot>
</a>

To distribute elements across component layout, we can use named slots. For example, template of my-container component:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

And we can use it like following:

<my-container>
  <h1 slot="header">Title</h1>

  <p>Text for main content.</p>
  <p>More text for main content.</p>

  <p slot="footer">Footer content</p>
</my-container>

And component result output will be:

<div class="container">
  <header>
    <h1>Title</h1>
  </header>
  <main>
    <p>Text for main content.</p>
    <p>More text for main content.</p>
  </main>
  <footer>
    <p>Footer content</p>
  </footer>
</div>

Template Recepies

Conditional Rendering

To implement conditions in JavaScript we usually use if (if-else) statements. Inside of templates and JSX we can't use them directly and should use JavaScript operators.

if

For if statement we should use logical AND (&&) operator:

<template>
  <div class="page">
    ${someVar && $h`
      <p>Text will be visible when "someVar" is truthy</p>
    `}

    ${someVar === 1 && $h`
      <p>Text will be visible when "someVar" equals to 1</p>
    `}
  </div>
</template>
<script>
  export default () => {
    const someVar = 1;

    return $render;
  }
</script>

Same using JSX:

export default () => {
  const someVar = 1;

  return () => (
    <div class="page">
      {someVar && (
        <p>Text will be visible when "someVar" is truthy</p>
      )}

      {someVar === 1 && (
        <p>Text will be visible when "someVar" equals to 1</p>
      )}
    </div>
  )
}

if-else

For if-else we can use Ternary operator (?:) or combination of && and ! operators:

<template>
  <div class="page">
    ${someVar ? $h`
      <p>Text will be visible when "someVar" is truthy</p>
    <code> : $h</code>
      <p>Text will be visible when "someVar" is falsy</p>
    `}

    {someVar && (
      <p>Text will be visible when "someVar" is truthy</p>
    )}
    {!someVar && (
      <p>Text will be visible when "someVar" is falsy</p>
    )}
  </div>
</template>
<script>
  export default () => {
    const someVar = 1;

    return $render;
  }
</script>

Same using JSX:

export default () => {
  const someVar = 1;

  return () => (
    <div class="page">
      {someVar ? (
        <p>Text will be visible when "someVar" is truthy</p>
      ) : (
        <p>Text will be visible when "someVar" is falsy</p>
      )}

      {someVar && (
        <p>Text will be visible when "someVar" is truthy</p>
      )}
      {!someVar && (
        <p>Text will be visible when "someVar" is falsy</p>
      )}

    </div>
  )
}

Mapping Array To Elements

To map array to elements we use Array's .map() method:

<template>
  <div class="page">
    <ul>
    ${items.map((item) => $h`
      <li>${item}</li>
    `)}
    </ul>
  </div>
</template>
<script>
  export default () => {
    const items = [
      'item 1',
      'item 2',
      'item 3',
    ];

    return $render;
  }
</script>

Same using JSX:

export default () => {
  const items = [
    'item 1',
    'item 2',
    'item 3',
  ];

  return () => (
    <div class="page">
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  )
}