generated from avior/template-web-astro
392
src/libs/Hyperion.ts
Normal file
392
src/libs/Hyperion.ts
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
import { objectLoop, objectSize } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hyperion is a library that allow you to make changes to the HTML content by using a mix of `<template />` and `JSON` Reponse from API endpoints
|
||||||
|
*
|
||||||
|
* ex:
|
||||||
|
* ```html
|
||||||
|
* <button
|
||||||
|
* data-action="/api/v1/checkout" // the endpoint to use
|
||||||
|
* data-template="#pouet" // the template to use
|
||||||
|
* >Checkout</button>
|
||||||
|
* <template id="pouet"><p
|
||||||
|
* data-attribute="test" // the JSON path
|
||||||
|
* ></p></template>
|
||||||
|
* ```
|
||||||
|
* will place inside the <button> tag a <p> tag with the text of `test` from the JSON of the API endpoint
|
||||||
|
*
|
||||||
|
* this library is a mix between systems like React/Svelte (which have a lot of frontend handling) and HTMX (which return HTML entirely from the server)
|
||||||
|
*
|
||||||
|
* _Hyperion is a mix of HyperText(HTML) and Action_
|
||||||
|
*
|
||||||
|
* @author Florian Bouillon <contact@avior.me>
|
||||||
|
* @version 1.0.0
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
export default class Hyperion {
|
||||||
|
private static instance: Hyperion
|
||||||
|
private constructor() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setup the library for usage
|
||||||
|
*/
|
||||||
|
public static setup() {
|
||||||
|
if (!this.instance) {
|
||||||
|
this.instance = new Hyperion()
|
||||||
|
}
|
||||||
|
return this.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to manually trigger an element
|
||||||
|
* @param el the element to trigger
|
||||||
|
*/
|
||||||
|
public static trigger(el: HTMLElement) {
|
||||||
|
el.dispatchEvent(new Event('hyperion:trigger'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialise hyperion
|
||||||
|
* @param base the base to query from
|
||||||
|
*/
|
||||||
|
private init(base?: HTMLElement) {
|
||||||
|
(base ?? document).querySelectorAll<HTMLElement>('[data-action]').forEach((it) => {
|
||||||
|
this.initItem(it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize a single element
|
||||||
|
* @param it the element to initialize
|
||||||
|
*/
|
||||||
|
private initItem(it: HTMLElement) {
|
||||||
|
// get the trigger action
|
||||||
|
let trigger = it.dataset.trigger ?? 'click'
|
||||||
|
|
||||||
|
// the triggering options
|
||||||
|
const options: {
|
||||||
|
once?: boolean
|
||||||
|
load?: boolean
|
||||||
|
after?: number
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
// handle options
|
||||||
|
if (trigger.includes(' ')) {
|
||||||
|
const splitted = trigger.split(' ')
|
||||||
|
trigger = splitted.shift()!
|
||||||
|
for (const item of splitted) {
|
||||||
|
// item runs only once
|
||||||
|
if (item === 'once') {
|
||||||
|
options.once = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (item === 'load') {
|
||||||
|
options.load = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (item.includes(':')) {
|
||||||
|
const [key, value] = item.split(':', 2)
|
||||||
|
// trigger is done after {x}ms (if another trigger is not done)
|
||||||
|
if (key === 'after') {
|
||||||
|
options.after = parseInt(value!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
// the triggering function
|
||||||
|
let fn = () => {
|
||||||
|
if (options.after) { // handle options:after
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = undefined
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.processElement(it)
|
||||||
|
timeout = undefined
|
||||||
|
}, options.after)
|
||||||
|
} else {
|
||||||
|
this.processElement(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// handle event only happening once
|
||||||
|
if (options.once) {
|
||||||
|
it.removeEventListener('hyperion:trigger', fn)
|
||||||
|
it.removeEventListener(trigger, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// event can also manually be triggered using `hyperion:trigger`
|
||||||
|
it.addEventListener('hyperion:trigger', fn)
|
||||||
|
it.addEventListener(trigger, fn)
|
||||||
|
|
||||||
|
// on load run instantly
|
||||||
|
if (options.load) {
|
||||||
|
this.processElement(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process an element that has it's trigger triggered
|
||||||
|
* @param it the element to process
|
||||||
|
*/
|
||||||
|
private async processElement(it: HTMLElement) {
|
||||||
|
const action = it.dataset.action // get the URL to try
|
||||||
|
const method = it.dataset.method ?? 'GET' // get the method to use
|
||||||
|
const subPath = it.dataset.path // get a subPath if desired (dot-separated values)
|
||||||
|
let target = it.dataset.target ?? 'innerHTML' // indicate how the data is set
|
||||||
|
const templateQuery = it.dataset.template // get the template query
|
||||||
|
const multiple = it.hasAttribute('data-multiple') // get if the result should be an array
|
||||||
|
const params: Record<string, any> = {} // the request parameters
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PRE REQUEST HANDLING
|
||||||
|
*/
|
||||||
|
|
||||||
|
// handle mendatory elements that are not necessary set
|
||||||
|
if (!templateQuery || !action) {
|
||||||
|
it.dispatchEvent(new CustomEvent('hyperion:attributeError', {
|
||||||
|
detail: {
|
||||||
|
reason: 'data-template or data-action not found'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
throw new Error(`the data-template (${templateQuery}) or data-action (${action}) attribute is not set :(`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(action, window.location.href)
|
||||||
|
|
||||||
|
// query the remote template
|
||||||
|
const template = document.querySelector<HTMLTemplateElement>(templateQuery)
|
||||||
|
if (!template) {
|
||||||
|
it.dispatchEvent(new CustomEvent('hyperion:templateError', {
|
||||||
|
detail: {
|
||||||
|
reason: 'template not found'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
throw new Error(`the Template for the query ${templateQuery} was not found ! :(`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-process the target attribute
|
||||||
|
let targetEl: HTMLElement | null = it
|
||||||
|
|
||||||
|
// handle if the target has a querySelector needed
|
||||||
|
if (target.includes(' ')) {
|
||||||
|
// split the target and the query
|
||||||
|
let [newTarget, query] = target.split(' ', 2)
|
||||||
|
|
||||||
|
// set the new target
|
||||||
|
target = newTarget!
|
||||||
|
|
||||||
|
// handle if the query start with `this`
|
||||||
|
let base: HTMLElement | Document = document
|
||||||
|
if (query?.startsWith('this')) {
|
||||||
|
query.replace('this', '')
|
||||||
|
base = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// query the targetElement
|
||||||
|
targetEl = base.querySelector<HTMLElement>(query!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle if the target was not found
|
||||||
|
if (!targetEl) {
|
||||||
|
template.dispatchEvent(new CustomEvent('hyperion:targetError', {
|
||||||
|
detail: {
|
||||||
|
type: 'target not found',
|
||||||
|
data: target
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
throw new Error(`target not found ${target}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.tagName === 'INPUT') {
|
||||||
|
const value = (it as HTMLInputElement).value
|
||||||
|
if (value) {
|
||||||
|
params[(it as HTMLInputElement).name] = (it as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
} else if (it.tagName === 'FORM') {
|
||||||
|
const form = (it as HTMLFormElement)
|
||||||
|
for (const item of form.elements) {
|
||||||
|
const name = (item as HTMLInputElement).name
|
||||||
|
const value = (item as HTMLInputElement).value
|
||||||
|
if (value) {
|
||||||
|
params[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REQUEST HANDLING
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (method === 'GET' && objectSize(params) > 0) {
|
||||||
|
objectLoop(params, (value, key) => {
|
||||||
|
url.searchParams.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the request
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
body: method === 'POST' && objectSize(params) > 0 ? JSON.stringify(params) : null
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST REQUEST HANDLING
|
||||||
|
*/
|
||||||
|
|
||||||
|
// handle if the request does not succeed
|
||||||
|
if (res.status >= 400) {
|
||||||
|
it.dispatchEvent(new CustomEvent('hyperion:statusError', {
|
||||||
|
detail: {
|
||||||
|
statusCode: res.status
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
throw new Error(`request returned a ${res.status} error code :(`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform the response into JSON
|
||||||
|
let json = await res.json()
|
||||||
|
|
||||||
|
// if subPath was set get to the path described
|
||||||
|
if (subPath) {
|
||||||
|
for (const child of subPath.split('.')) {
|
||||||
|
json = json[child]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the template clone(s)
|
||||||
|
const clones: Array<HTMLElement> = []
|
||||||
|
|
||||||
|
// if the query should have multiple clones (data-multiple)
|
||||||
|
if (multiple) {
|
||||||
|
// the data should be an array :D
|
||||||
|
if (!Array.isArray(json)) {
|
||||||
|
it.dispatchEvent(new CustomEvent('hyperion:dataError', {
|
||||||
|
detail: {
|
||||||
|
type: 'data_not_array',
|
||||||
|
data: json
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
throw new Error(`the data inputted should be an array, instead it is an ${typeof json}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through the array
|
||||||
|
for (const item of json) {
|
||||||
|
// fill the template element with every values
|
||||||
|
clones.push(this.fillTemplate(template, item))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fill the template element with the data
|
||||||
|
clones.push(this.fillTemplate(template, json))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle how the clones are placed
|
||||||
|
switch (target) {
|
||||||
|
case 'outerHTML': // the clones replace the targetted element
|
||||||
|
targetEl.replaceWith(...clones)
|
||||||
|
break;
|
||||||
|
case 'innerHTML': // the clones replace the childs of the targetted element
|
||||||
|
// remove the current childs
|
||||||
|
while (targetEl.firstChild) {
|
||||||
|
targetEl.removeChild(targetEl.firstChild)
|
||||||
|
}
|
||||||
|
// add each clones
|
||||||
|
clones.forEach((it) => targetEl.appendChild(it))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fill the template from the data given
|
||||||
|
*
|
||||||
|
* @param template the template to fill
|
||||||
|
* @param data the data to filel with
|
||||||
|
* @returns the filled HTMLElement
|
||||||
|
*/
|
||||||
|
private fillTemplate(template: HTMLTemplateElement, data: any): HTMLElement {
|
||||||
|
// clone the template
|
||||||
|
const clone = template.content.cloneNode(true) as HTMLElement
|
||||||
|
|
||||||
|
// go through every elements that has a attribute to fill
|
||||||
|
clone.querySelectorAll<HTMLElement>('[data-attribute]').forEach((it) => {
|
||||||
|
// get the raw attribute
|
||||||
|
const attrRaw = it.dataset.attribute!
|
||||||
|
|
||||||
|
// parse into an array
|
||||||
|
let attrs: Array<string>
|
||||||
|
if (attrRaw.includes(' ')) {
|
||||||
|
attrs = attrRaw.split(' ')
|
||||||
|
} else {
|
||||||
|
attrs = [attrRaw]
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through each attributes
|
||||||
|
for (let attr of attrs) {
|
||||||
|
// get the display type of the attribute
|
||||||
|
let target = it.dataset.type ?? 'text'
|
||||||
|
|
||||||
|
// detect if it should be set as an attribute instead
|
||||||
|
const setToAttribute = attr.includes(':')
|
||||||
|
if (setToAttribute) {
|
||||||
|
const splitted = attr.split(':', 2)
|
||||||
|
attr = splitted.pop()!
|
||||||
|
target = splitted[0]!
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle processing of string
|
||||||
|
let content: string
|
||||||
|
// handle a string with attribute processing
|
||||||
|
if (attr.includes('{')) {
|
||||||
|
let res = /\{(.*?)\}/g.exec(attr)
|
||||||
|
while (res) {
|
||||||
|
attr = attr.replace(`${res[0]}`, data[res[1]!])
|
||||||
|
res = /\{(.*?)\}/g.exec(attr)
|
||||||
|
}
|
||||||
|
content = attr
|
||||||
|
} else { // handle basic string
|
||||||
|
content = attr === 'this' ? data : data[attr]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setToAttribute) { // set it as attribute
|
||||||
|
it.setAttribute(target, content)
|
||||||
|
} else {
|
||||||
|
// get the real target function
|
||||||
|
if (target === 'html') {
|
||||||
|
target = 'innerHTML'
|
||||||
|
} else if ( target === 'text') {
|
||||||
|
target = 'innerText'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(target in it)) {
|
||||||
|
template.dispatchEvent(new CustomEvent('hyperion:dataError', {
|
||||||
|
detail: {
|
||||||
|
type: 'data-type invalid',
|
||||||
|
data: target
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
throw new Error(`the data-type is not valid (${target}), it should be one of ('innerHTML' | 'innerText' | 'outerHTML' | 'outerText')`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set it has inner/outer HTML/Text
|
||||||
|
it[target as 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText'] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// idk if necessary but remove the attributes from the final HTML
|
||||||
|
it.removeAttribute('data-attribute')
|
||||||
|
it.removeAttribute('data-type')
|
||||||
|
})
|
||||||
|
|
||||||
|
// setup the clone to work if it contains Hyperion markup
|
||||||
|
this.init(clone)
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,90 @@
|
|||||||
# Libs
|
# Libs
|
||||||
|
|
||||||
Globally independent objects/classes/functions that SHOULD be unit testable by themselve
|
Globally independent objects/classes/functions that SHOULD be unit testable by themselve
|
||||||
|
|
||||||
|
## Hyperion
|
||||||
|
|
||||||
|
Hyperion is a library that allows you to make change to the HTML content dynamically depending on the results of a JSON request
|
||||||
|
|
||||||
|
it is kinda a mix of React/Svelte and HTMX
|
||||||
|
|
||||||
|
it use both `data-` items and `<template />` elements
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- define your input -->
|
||||||
|
<button
|
||||||
|
data-action="/api/v1/checkout" // the endpoint to use
|
||||||
|
data-template="#pouet" // the template to use
|
||||||
|
>Checkout</button>
|
||||||
|
|
||||||
|
<!-- Define the template to fill -->
|
||||||
|
<template id="pouet"><p
|
||||||
|
data-attribute="test" // the JSON path
|
||||||
|
></p></template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
|
||||||
|
**on Item Attributes**
|
||||||
|
|
||||||
|
| nom | default | Type | Valeurs possible | Description |
|
||||||
|
| ------------- | --------- | ------- | ------------------------------- | -------------------------------------------------------- |
|
||||||
|
| data-trigger | click | Texte | Standard Event, load, after:xxx | see Triggers |
|
||||||
|
| data-action | | Texte | | the url to fetch |
|
||||||
|
| data-method | GET | Texte | GET, POST, PUT, DELETE | the method to use |
|
||||||
|
| data-path | | Texte | | the subpath to start from in the template |
|
||||||
|
| data-target | innerHTML | Texte | innerHTML, outerHTML | the target element, see Target |
|
||||||
|
| data-template | | Texte | CSS Query | the template CSS query |
|
||||||
|
| data-multiple | | Boolean | | if set, it will run on an array, else on everything else |
|
||||||
|
|
||||||
|
**In template Attributes**
|
||||||
|
|
||||||
|
| nom | default | Type | Valeurs possible | Description |
|
||||||
|
| -------------- | ------- | ---- | ---------------- | -------------------------------------------- |
|
||||||
|
| data-attribute | | Text | | the attribute to get from the fetched object |
|
||||||
|
| data-type | text | Text | | text, html, outerHTML, outerText |
|
||||||
|
|
||||||
|
### Triggers
|
||||||
|
|
||||||
|
To make a trigger you can define a WebStandard HTML Event like `click`, `mouseover`, etc and it will work after it is emitted by the browser.
|
||||||
|
|
||||||
|
there is also a special `load` trigger that will run hyperion on the tag after the page has loaded
|
||||||
|
|
||||||
|
you can have multiple triggers by adding them all with a space between (ex: `data-trigger="click, blur"`)
|
||||||
|
|
||||||
|
there is also modifiers:
|
||||||
|
- `once`: the request will run only once and never again after (note: `load` trigger don't count for this)
|
||||||
|
- `after:x`: Dedupplicate the request sent to the server by waiting at least `x` millis with not triggering done
|
||||||
|
|
||||||
|
|
||||||
|
### Targets
|
||||||
|
|
||||||
|
Targets defin where the generated content will be placed.
|
||||||
|
|
||||||
|
- `innerHTML`: it will be placed as a child of the current element
|
||||||
|
- `outerHTML`: it will replace the current element at the same position (making it runnable once)
|
||||||
|
|
||||||
|
You can also as a second argument change le location of the target by usin a CSS query
|
||||||
|
|
||||||
|
example:
|
||||||
|
```html
|
||||||
|
<button
|
||||||
|
...
|
||||||
|
data-target="innerHTML #pouet"
|
||||||
|
>Button</button>
|
||||||
|
|
||||||
|
<p id="pouet"></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally you can run the query from the current element you can prepend the query by `this`
|
||||||
|
example:
|
||||||
|
```html
|
||||||
|
<div
|
||||||
|
...
|
||||||
|
data-target="innerHTML this.pouet"
|
||||||
|
>
|
||||||
|
<p class="pouet"></p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
10
src/pages/api/v1/checkout.ts
Normal file
10
src/pages/api/v1/checkout.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
|
|
||||||
|
function randomInt(min = 0, max = 10) {
|
||||||
|
return Math.round(Math.random() * max + min)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ALL: APIRoute = ({ url }) => {
|
||||||
|
return new ResponseBuilder().body({caca: 'pokemon', list: [url.searchParams.get('filter'), ...Array(randomInt(1, 20)).fill(0).map(() => randomInt(10, 100))]}).build()
|
||||||
|
}
|
21
src/pages/api/v1/projects/index.ts
Normal file
21
src/pages/api/v1/projects/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: APIRoute = ({ url }) => {
|
||||||
|
const nameFilter = url.searchParams.get('name')
|
||||||
|
return new ResponseBuilder().body(([
|
||||||
|
{
|
||||||
|
name: 'Holisané',
|
||||||
|
id: 'HOLIS'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Aptatio',
|
||||||
|
id: 'APTA'
|
||||||
|
}
|
||||||
|
] as Array<Project>).filter((it) => !nameFilter || it.name.includes(nameFilter))).build()
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
import Button from 'components/global/Button.astro'
|
||||||
|
import Input from 'components/global/Input.astro'
|
||||||
import MainLayout from 'layouts/MainLayout.astro'
|
import MainLayout from 'layouts/MainLayout.astro'
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -6,5 +8,56 @@ import MainLayout from 'layouts/MainLayout.astro'
|
|||||||
<main class="container flex flex-col justify-center items-center h-screen gap-6">
|
<main class="container flex flex-col justify-center items-center h-screen gap-6">
|
||||||
<h1 class="text-8xl text-center">Dzeio Astro Template</h1>
|
<h1 class="text-8xl text-center">Dzeio Astro Template</h1>
|
||||||
<h2 class="text-2xl text-center">Start editing src/pages/index.astro to see your changes!</h2>
|
<h2 class="text-2xl text-center">Start editing src/pages/index.astro to see your changes!</h2>
|
||||||
|
<Button
|
||||||
|
data-action="/api/v1/checkout"
|
||||||
|
data-template="#pouet"
|
||||||
|
data-trigger="mouseenter after:100"
|
||||||
|
data-target="outerHTML"
|
||||||
|
>Checkout</Button>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
data-action="/api/v1/projects"
|
||||||
|
data-template="#projectItem"
|
||||||
|
data-trigger="keydown load after:100"
|
||||||
|
data-target="innerHTML ul"
|
||||||
|
data-multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>Results</p>
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
|
<template id="projectItem">
|
||||||
|
<li>
|
||||||
|
<a data-attribute="name href:/project/{id}"></a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<!-- Define a template which contains how it is displayed -->
|
||||||
|
<template id="pouet">
|
||||||
|
<div>
|
||||||
|
<!-- Define the attribute in the object that will be assigned -->
|
||||||
|
<p data-attribute="caca"></p>
|
||||||
|
<!-- You can change the display type by selecting `html` | `innerHTML` | `outerHTML` | `text` | `innerText` | `outerText` -->
|
||||||
|
<p data-attribute="caca" data-type="html"></p>
|
||||||
|
<!-- You can even add another Requester inside the template! -->
|
||||||
|
<p
|
||||||
|
data-attribute="caca value:caca"
|
||||||
|
|
||||||
|
data-action="/api/v1/checkout"
|
||||||
|
data-template="#pouet"
|
||||||
|
data-trigger="mouseover"
|
||||||
|
data-target="outerHTML"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template id="list">
|
||||||
|
<li data-attribute="this"></li>
|
||||||
|
</template>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Hyperion from 'libs/Hyperion'
|
||||||
|
|
||||||
|
Hyperion.setup()
|
||||||
|
</script>
|
||||||
|
Reference in New Issue
Block a user