organic front-end components as custom HTML tags
Check out
npm i organic-oval webpack babel-loader @babel/core @babel/plugin-transform-react-jsx
const webpack = require('webpack')
module.exports = {
'resolve': {
'extensions': ['.webpack.js', '.web.js', '.js', '.tag'],
},
'module': {
'rules': [
{
test: /\.tag$/,
use: {
loader: 'babel-loader',
options: {
plugins: [ require.resolve('@babel/plugin-transform-react-jsx') ]
}
}
},
{
test: /\.tag$/,
use: [
{loader: require.resolve('organic-oval/webpack/oval-loader')}
]
}
]
}
}
<!-- ./index.html -->
<html>
<body>
<my-app></my-app>
<script src='./bundle.js'></script>
</body>
</html>
// ./bundle.js
require('./components/my-app.tag')
<!-- ./my-app.tag -->
<my-app>
<h1>Welcome!</h1>
</my-app>
<navigation>
<script>
this.state.links = {
home: '#home',
about: '#about'
}
</script>
<ul class="navigation">
<li><a href={this.state.links.home}>Home</a></li>
<li><a href={this.state.links.about}>About</a></li>
</ul>
</navigation>
- The tag name must be unique for global components. See also Nesting local components section bellow.
- The script is optional and contains the component's constructor logic.
<!-- ./navigation.tag -->
<navigation>
<script>
require('./navigation-item')
...
</script>
...
<navigation-item ...>...</navigation-item>
...
</navigation>
<!-- ./navigation-item.tag -->
<navigation-item not-global>
...
</navigation-item>
<!-- ./navigation.tag -->
<navigation>
<script>
const MyItem = require('./navigation-item')
...
</script>
...
<MyItem ...>...</MyItem>
...
</navigation>
<!-- ./my-table-row.tag -->
<my-table-row>
...
</my-table-row>
<!-- ./my-table.tag -->
<my-table>
<script>
const MyTableRow = require('./my-table-row')
...
</script>
...
<table>
<tbody>
<MyTableRow as='tr'>...</MyTableRow>
</tbody>
</table>
...
</my-table>
<!-- ./navigation.tag -->
<navigation>
<script>
require('./navigation-item')
this.itemValue = 'item'
this.obj = {with_references: true}
this.handler = (value) => {
console.log(value) // 'response'
}
this.clickHandler = (e) => {}
</script>
...
<navigation-item
d-value={this.itemValue}
obj={this.obj}
eventName={this.handler}
onclick={this.clickHandler} />
...
</navigation>
<!-- ./navigation-item.tag -->
<navigation-item>
<script>
console.log(this.props['d-value']) // "item"
console.log(this.props.obj) // {with_references: true}
this.emit('eventName', 'response') // triggers eventName handlers
this.on('mounted', () => {
this.el.triggerEvent(new Event('click')) // triggers click
})
</script>
<div></div>
</navigation-item>
<!-- ./my-container.tag -->
<my-container>
<slot name='header' />
<slot name='content' />
<slot name='footer'>
<div>default footer</div>
</slot>
</my-container>
<!-- ./app.tag -->
<app>
<my-container>
<h2 slot='content'>inner content 1</h2>
</my-container>
<my-container>
<loggedin-header slot='header' />
<main slot='content' />
</my-container>
</app>
<navigation>
<script>
this.show = false
</script>
<h1 if={this.show}>
H1 Text
</h1>
</navigation>
<navigation>
<script>
this.items = [1, 2, 3]
</script>
<ul>
<each itemValue, itemIndex in {this.items}>
<li>{itemIndex} - {itemValue}</li>
</each>
</ul>
</navigation>
The following tag won't re-render itself and will not be replaced by parent tag updates as long as it is instantiated and part of the dom.
<my-tag>
<script>
tag.on('mounted', function () {
tag.shouldRender = false
})
</script>
</my-tag>
<!-- custom-element.tag -->
<custom-element>
<script>
this.on('mounted', () => {
let el = this.el.querySelector('another-custom-element')
let component = el.component
console.log(component) // reference to another-custom-element component
})
</script>
<another-custom-element />
</custom-element>
The method defines a component accordingly to its options:
tagName
: StringtagLine
: Stringonconstruct
: Function
tagName
, to skip that action provide a tagLine
with not-global
attribute.
/** @jsx createElement */
require('organic-oval').define({
tagName: 'my-tag',
onconstruct: function () {
// this equals to OvalComponent instance and is executed
// during component construction
}
})
The method upgrades given NodeElement el
accordingly to previously defined
component with matching el.tagName
.
Returns reference to the newly created component. Calling the method also sets el.component
reference to the created component.
let el = document.createElement('my-tag')
document.body.appendChild(el)
require('organic-oval').upgrade(el)
Every Oval component extending preact
Component. All methods are inherited thereafter and you should refer to preact
's api refernce as well
Appends component instance at respective root.
const MyComponent = require('./my-component.tag')
MyComponent.appendAt(document.body, props)
Returns reference to the rendered component's element.
Instructs to do a forceUpdate
of the component. Fires update
related events.
Removes the component and its custom element from dom. Fires unmount
events.
Start receiving emitted events. By default Oval Components self emit their Lifecycle events as follows:
mount
- only once on mountupdate
- every time when tag is updated (respectively on first mount too)updated
- every time after tag is updated and rendered to dommounted
- only once tag is mounted and updated into domunmounted
- when tag is removed from dom
Additionally any functions been passed to oval components will automatically subscribe:
<my-container>
<script>
this.myCustomEventHandler = function (eventData) {
// triggered every second by `my-component`
}
</script>
<my-component customEvent={this.myCustomEventHandler} />
</my-container>
<my-component>
<script>
this.on('mounted', () => {
setInterval(() => {
this.emit('customEvent', 'customEventData')
}, 1000)
})
</script>
</my-component>
Stop receiving events
Publish eventName
with optional eventData
to all subscribers.
Append the Component to an existing DOM element and provide the relevant component's props
<!-- ./my-component.tag -->
<my-component> ... </my-component>
<!-- ./some-other-file.js -->
const MyComponent = require('./my-component')
let rootEl = ... // a jquery selector or document.querySelectorAll
let props = { myProp: value, class: 'css-class' }
MyComponent.appendAt(rootEl, props)
this.props
is undefined during component construction and is available aftermount
event.this.emit
emits only to component's listeners without delegation to dom events
- element declaration with
if
attribute will work only when if statement is on the first line
will NOT work:
<h1 class='test'
if=${condition}>
Some Text
</h1>
will work as expected:
<h1 if=${condition}
class='test'>
Some Text
</h1>
- each loops will work only when looped node is declared on the next line
will NOT work:
<each model in ${items}><looped-content>${model}</looped-content></each>
will work as expected:
<each model in ${items}>
<looped-content>${model}</looped-content>
</each>
- fix known compiler issues
- implement more plugins for other source code bundlers
Any help and PRs are welcome ❤️