Giter Site home page Giter Site logo

ef.js's Introduction

ef.js

GitHub license npm Release Packages FOSSA Status

(maybe) An elegant HTML template engine & basic framework

ef.js is a static template framework for browsers, with which you can write your UI without concerning about the logic, or writing logic without concerning about the UI.

ef.js also provides a simple template-engine which helps you create component modules with data binding at ease, but you can also use your favourite template-engine which is compatible with ef.js's AST.

ef.js is well compatible with WebComponents, and is probably the only existing front-end framework that handles XML namespaces properly.

Official Website (WIP)

Sponsors and Backers

See here.

Demo:

Playground:

Related projects:

Community projects:

Implementation in other languages:

  • ef.qt Writing Qt applications using the concept of ef

CDN

CDNJS | jsDelivr | UNPKG

For dev versions:

CDNJS | jsDelivr | UNPKG

Usage

import { create, onNextRender, inform, exec, bundle, setParser, parseEft, t, version } from 'ef.js'
// or you can use import * as ef from 'ef.js'

version // Version string of ef.js

setParser(someparser) // Change the default parser for ef.js so you can use a different type of template
parseEft('Your awesome template') // Get ef.js ast using default parser

const templateString = 'Your awesome template'
const ast = [/* AST which supported by ef */]

const data = {
  $data: {/* Binding data */},
  $methods: {/* Binding methods */}
}

const template1 = create(template)
const template2 = create(ast)
const template3 = t`
>component1
>component2
.Your awesome template
`

const component1 = new template1() // Create a component without data
const component2 = new template2(data) // Create a component and then updates it's data
const component3 = new template3(data, {component1, component2}) // Use component1 and component2 as custom components in template3

onNextRender(callback) // Cache operations to execute on next render
inform() // Tell ef to cache operations **USE WITH CARE**
exec() // Tell ef to execute all cached operations **USE WITH CARE**
exec(true) // Force execute cached operations **USE WITH CARE**
bundle(callback) // Wrapper for inform() and exec()

component1.$data.something = 'something new' // Update the binding data 'something'
component2.$methods.someMethod = ({e, value, state}) => {
  state.$data.something = 'something new'
  console.log('Event target', e.target)
  console.log('Value passed', value)
} // Update binding method

const logData = val => console.log('Subscribed data updated:', val)
component1.$subscribe('info.data', logData) // Observe a value
component1.$unsubscribe('info.data', logData) // Stop observing a value

component1.$update(data) // Update the whole component state

component1.$refs // Get all referenced nodes

component1.mountingPoint = component2 // Mount component2 to 'mountingPoint' on component1
component1.mountingPoint = null // Detach the mounted component

component1.listMP.push(componet2) // Mount component2 to list 'listMP' mounting point on component1

component1.$mount(...) // Mount method called by ef when trying to mount
component1.$umount() // Unmount from parent
component1.$destroy() // Destroy the component when not needed for more memory

ef.js template language (EFML) format

EFML is a completely logic-free template language. Just like HTML, there you can do nothing about logic, but EFML provides a easy starting point for data binding and events handling.

Also EFML is the first language that can be parsed into the AST which ef supports.

Note: EFML is very strict to indents. Wrong indents could lead to a parsing error.

Here is an example.

Lines not start with >#%@.|+- are comments.
For example, this line is a comment.
The escape character of EFML is `&`, for prevention of conflicts with js escapes.

Lines starting with '>' stand for a new tag, for example, a `div` tag can be written as:
>div

`.` following a tag definition is the short hand for writing class names, for example:
>div.my.class.name
would be rendered as `<div class="my class name">`

Class names can have mustaches mixed together:
>div.my.{{dynamic}}.class.name
would be rendered as `<div class="my class name">` when `instance.$data.dynamic` is empty, `<div class="my dynamic class name">` when `instance.$data.dynamic` is 'dynamic'

`#` at the end of a tag definition means the reference name for this element, for example:
>div#myDiv
this `div` tag is now accessable form `instance.$refs.myDiv`.

New lines with exactly one indent after a tag definition are all things that belong to the defined tag, for example:
>div
  #attribute = myAttr
  %property = myProp
  @event = myEventHandler
  >ChildElement

  Lines starting with '#' stand for attributes

  This means an attribute without any parameters:
  #flagattr

  Contents without mustaches stand for static data:
  #id = myID
  #some-attr = some text

  Mustaches are used for binding data:
  #class = {{myClass}}
  The element's class name can then be set via `instance.$data.myClass = 'some class names'`

  Contents inside mustaches after '=' stand for the default value for this binding:
  #class = {{myClass = some class name}}
  #style = {{attr.style = background: #ECECEC}}

  Static content and mustaches can be mixed:
  #class = static and {{mixed}} classes

  Lines starting with '%' stand for properties that can be accessed from the DOM object
  %title = Welcome, {{name}}
  %anotherProperty = text

  Lines starting with '@' stand for events that triggers an event handler method:
  @click = clickHandler

  Contents after ':' are values to be passed to the handler:
  @click = updateInfo:{{binding.value}} and static value

  modifier keys now can bind with predefined aliases:
  @mousedown.shift.alt.ctrl.meta = select

  bind to specific key code by writing the code directely:
  @keypress.13 = submit

  use '.prevent' for `preventDefault`, '.stop' for `stopPropagation`, '.stopImmediate' for `stopImmediatePropagation`
  use '.passive' to make the event listener passive, '.!passive' to explicitly claim a non-passive listener
  use '.once' to create a trigger once listener
  @keydown.8.prevent.stop = stopbackspace

  use '.capture' to capture an event
  @submit.capture.stopImmediate = submit

  Lines starting with '.' stand for text nodes
  .Name: {{name}}&nJob: {{job}}

  >pre
    Lines starting with '|' stand for multiline text
    |Line 1
    |Line 2
    |Line 3

  Lines starting with '-' stand for single node mounting point
  -node1
  use `instance.node1 = anotherInstance` to put another EF component right at the point.

  Lines starting with '+' stand for multi node mounting point
  +list1
  use `instance.list1.push(...newInstances)` to put other EF components here.

For standalone eft parser see eft-parser.

Fragments

After version 0.9.0, ef.js now supports fragments, which requires eft-parser to be v0.9.0 and above. A normal template could only have one entry tag, while fragment templates can have multiple, even mounting points can be put at root level:

>div
  .A root level tag
-rootLevelMountingPoint
>p
  .Another root level tag
+rootLevelListMountingPoint
.Root level text node

You can use them just like normal templates, behaviors are always the same. Also, a single text node will be treated as fragments as well.

Helpers

ef.js also provides some helpers for creating Fragments and EFTextFragments, or transform almost anything into an ef component.

// Creats a fragment containing given ef components, non ef components will be automatically transtormed into ef components.
new ef.Fragment(Array<Any>)
// Creats a single `TextFragment` which contains only the given text. Text on `EFTextFragment` components can be modified with `.text` property.
new ef.EFTextFragment(string)
// Converts almost anything into an ef component
ef.toEFComponent(Any)

Attribute Mapping

Data on ef.js components are not always that easy to access, so since v0.10.4, a stable version of attribute mapping helper is bundled with ef.js. For documents, please refer to the comments for now. It would be extremely useful when using with custom components.

Custom Components

ef.js can handle custom components in templates since v0.10.4. Demo on writing logic within ef template using custom component

Scope

Scoping is not done in templates. You can write your template as normal, using whatever tag name you desire for your custom component, like:

App.eft
>div#root
  >MyComponent#myComponent
  >MyOtherComponent

Then you may pass the scope in your script:

import App from 'App.eft'
import MyComponent from 'MyComponent.eft'
import MyOtherComponent from 'MyOtherComponent.eft'

const scope = {MyComponent, MyOtherComponent}
const app = new App(null, scope)

If scope is not given when initializing the component, ef will treat these custom tags as normal HTML tags.

Note that if you reference a custom component, you'll get the component instance instead of the component's DOM object:

app.$refs.root // DOM object
app.$refs.myComponent // ef component

Scoping can also be used to replace some tags. Like:

const scope = {
  MyComponent: 'div',
  MyOtherComponent: {
    tag: 'div',
    is: 'my-web-component'
  }
}

MyComponent will be rendered as a normal div element, while MyOtherComponent will be rendered as a my-web-component.

Attributes

Attributes on custom components are mapped to component[key], single way:

App.eft
>MyComponent#myComponent
  #myAttribute = {{customAttr}}
app.$data.customAttr = 'Lorem ipsum...' // This will actually set app.$refs.myComponent.myAttribute

Properties

Properties on custom components are mapped to component.$data[key], single way:

App.eft
>MyComponent#myComponent
  %my.Property = {{customProperty}}
app.$data.customProperty = 'Lorem ipsum...' // This will actually set app.$refs.myComponent.$data.my.Property

Events

Event handling only works on custom emitted events on custom component:

App.eft
>MyComponent#myComponent
  @myEvent = handleMyEvent
app.$refs.myComponent.$emit('myEvent') // This will trigger `handleMyEvent`

Note that modifier keys are no longer able to present on custom emitted events, so dont attach modifier key on them.

Automatic Two Way Binding

Just like what ef requires HTML elements to do to get custom two way binding, a value or checked property should present on a custom component, together with an input or keyup or change event been emitted when value has been changed. When binding checked, only change event should be emitted.

App.eft
>MyComponent
  %value = {{value}}
MyComponent.eft
>input
  #type = text
  @input = handleInput
import {mapAttrs} from 'ef.js'
import App from 'App.eft'
import _MyComponent from 'MyComponent.eft'

const MyComponent = class extends _MyComponent {
  constructor(...args) {
    super(...args)
    this.$methods.handleInput = ({state}) => {
      state.$emit('input')
    }
  }
}

const app = new App(null, {MyComponent}) // $data.value will automatically updats with what was changed in MyComponent

Custom Two Way Binding

Custom two way binding was implemented since v0.13.0. What you need to do is simply adding an @custom-trigger-event after your propertp definition. For example:

App.eft
>MyComponent#myComponent
  %customProp@valueChange = {{value}}
// App.js

import App from 'app.eft'

const app = new App()

app.$refs.myComponent.$emit('valueChange')

This will trigger an automatic child-to-parent value sync.

Update Only Binding

Adding a ! after prop path will mark this prop will not receive any value changes from JavaScript part. Which is useful when needed to get some non-writable values on DOM objects:

>input
  %value! = {{value}}

this will make the input only accepts value changes that are made by user inputs.

Sync Only Binding

Adding a & after the value will not automatically create two way binding for this property:

>input
  %value = {{value}}&

this will make the input not automatically send changes to $data.value, while modifications made on $data.value will still be reflected on the input element.

Children

You can write custom components with children just like what you do with normal HTML elements:

>MyComponent
  >div
  >MyOtherComponent
  -mountingPoint
  +listMountingPoint

but with one requirement: the custom component that handles children should have a list mounting point or an attribute named children:

MyComponent.eft
>div.my-field-set
  >span
    .{{legend}}
  +children

XML Namespaces

ef.js now handles custom XML namespaces since v0.12.0, which allows ef.js to be able to handle svg fragments easily.

Namespace Usage

Just like what you do in XML:

Render this `div` tag under a given namespaceURI
>div
  #xmlns = http://some.namespace.example.com/myns
Render these tags with given namespaceURI by local prefix
>myns:div
  #xmlns:myns = http://some.namespace.example.com/myns
  >myns:h1
  >myns:table
Render only parts of an SVG element
>svg:path
Render only parts of an MathML element
>math:matcion

You must declare your namespace prefix before using your prefix either globally or locally.

Global Namespaces

ef.js has 5 built-in global namespaces, which are xml for XML namespace itself, html for HTML elements, svg for SVG elements, math for MathML elements and xlink for xlink attributes. You can register custom global namespaces using declareNamespace:

import {declareNamespace} from 'ef.js'

declareNamespace('myns', 'http://some.namespace.example.com/myns')

Then you can use it everywhere across the whole project.

Note:

  1. Using global namespaces with prefix will make it's children also inherit it's namespace.
  2. Re-declareation of the same prefix will throw out an error.

Local Namespaces

ef.js supports a XML-like local namespace declaration:

>myns:div
  #xmlns:myns = http://some.namespace.example.com/myns

Then you can use the namespace across the whole template.

Note:

  1. What efml namespaces differs from actual XML local namespaces is that in XML it works only for itself or it's children, while in efml this declaration works across the whole template no matter where you define it.
  2. Local defined namespaces only works when a tag is prefixed with the defined prefix. Children of it will not inherit the namespace.
  3. Re-declaration will NOT give en error.

Namespacing in Scopes

When an tag is scoped, it will use the scoped tag's namespace, if the tag has no prefix, it will use the original xmlns instead if only namespaceURI is not set on scope option. Scoped prefix has higher priority than namespaceURI. For example:

template.eft
>div
  .This tag will be scoped.
import Tpl from './template.eft'

const scope1 = {
  div: 'myns:div'
}

const component1 = new Tpl(null, scope1) // in this case it will render a `div` under `myns`

const scope2 = {
  div: {
    tag: 'myns:div',
    namespaceURI: 'http://some.other.ns/myns'
  }
}

const component2 = new Tpl(null, scope2) // in this case the `namespaceURI` is completely ignored, element is rendered under `myns`

const scope3 = {
  div: {
    namespaceURI: 'http://some.other.ns/myns'
  }
}

const component3 = new Tpl(null, scope3) // in this case the `namespaceURI` is not ignored, element is rendered under `http://some.other.ns/myns`

Initialization API

Initialization APIs are added since v0.17.0.

NOTE: state and $data are not fully initialized yet upon initlization. They're passed here only for reference. You should retrive any mount point or reactive value within your handler methods.

import Tpl from './template.eft'
import Component from './my-component.eft'

const App = class extends Tpl {

  // Prepare initial methods. The return value will be used as-is.
  static initMethods(state, $data, watch) {
    let count = 0
    return {
      clickBtn() {
        count += 1
        $data.count = count
        alert(`You have clicked ${count} times!`)
      }
    }
  }

  // Prepare initial data. The return value is non-reactive.
  static initData(state, $data, watch) {
    // Watch is equivalent for '$subscribe', but you have to use `watch` instead during initlization.
    watch('count', ({value}) => {
      console.log('Count has changed to', value)
    })

    return {
      count: 'You have not clicked.',
      btnText: 'Click Me!'
    }
  }

  // Prepare scope. The return value will be merged and supress previous assigned values.
  static initScope(state, $data, watch) {
    return {
      // In this case, all `div` will be rendered as `h1`, all `MyComponent` will be rendered as `Component`
      div: 'h1',
      MyComponent: Component
    }
  }

  // Notice: overriding `init` will supress all above methods.
  static init(state, $data, watch) {
    return {
      methods: {},
      data: {},
      scope: {},
      beforeMount() {},
      afterMount() {},
      beforeUmount() {},
      afterUmount() {},
      beforeDestroy() {},
      afterDestroy() {},
      onCreated() {}
    }
  }
}

JSX

JSX support was removed since v0.17.0.

Server Side Rendering

JavaScript implemented DOM

Use domino or JSDom for DOM implementation. Only Node and document is required for customization.

For example using domino:

const ef = require('ef.js')
const domino = require('domino')

ef.setDOMImpl({
  Node: domino.impl.Node,
  document: domino.createDocument()
})

Then you can use it as it is in browser.

undom is currently not supported because it's lack of documentFragment support, but an undom-ef fork has made it possible.

Check here for an exapmle.

Implementation without DOM

Currently there's no full featured implementation for ef.js to run without a DOM, but some experiments have been done. If you want speed or do not need full featured ef, you can try the static-ef-raw method.

Since ef sacrifices initialization time for faster updating, if you are not re-generating your page every second, the undom-ef-super-cached method is the most recommended since it gives you an almost fully featured ef.js in server environment while keeping the footprint small, yet still fast enough compared to other frameworks.

Typing Support

HELP WANTED

ef.js now has partial experimental typing support using TypeScript flavored JSDoc, which should be compatible with TypeScript. See ef-core and ef.js.

Run a test

git clone https://github.com/ClassicOldSong/ef.js.git
cd ef.js
npm install
npm start

Then you can test it out in the opening browser window.

Build from source

git clone https://github.com/ClassicOldSong/ef.js.git
cd ef.js
npm install
npm run build && npm run prod

Then you can get the fresh-built ef.min.js in the dist folder.

Note: All debugging messages are disabled in the production version.

License

MIT

FOSSA Status

ef.js's People

Contributors

classicoldsong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ef.js's Issues

Multiline text support with EFML

Add a symbol | to support multiline with automatically new line support, no need to add a &n at each line like using . strings.

Example:

>pre
  .This is a single line content.
  . This line of text is following the first line.
  |This starts a new line by automatically adding an `&n` to the beginning of the line.
  |A new line is started.
  |Another new line is started.
  . This follows the previous line. 

Custom two-way binding support

Such as

>input
  #type = date
  %valueAsDate@change = {{date}}

When change event triggers, date will automatically update to the latest inputElement.valueAsDate.

Add eft-compiler

Add a relay-compiler like compiler for ef templates to support strong typing.

Add JSX support

Jsx is now widely used in multiple frontend frameworks like React and Vue.js. After a while of thinking I found that having jsx in ef.js projects is pretty possible. Here is an example:

const {t} = ef

const MyDiv = t`
>div.{{className}}
  #my-component
  #style = {{style}}
  +children
`

const Hello = t`
>h1.{{className}}
  #my-component
  #style = {{style}}
  .Hello, {{name}}!
`

const MyTextInput = t`
>input.{{className}}
  #my-component
  #type = text
  #style = {{style}}
  @input = input:{{value}}
  %value = {{value}}
`

const myHelloInstance = new Hello()

const myComponent = <MyDiv
  $data={{
    className: 'some class names',
    style: 'background-color: #CCC;'
  }}
>
  {myHelloInstance},
  <MyTextInput
    $methods={{
      input({value}) {
        myHelloInstance.$data.name = value
      }
    }}
  />
</MyDiv>

myComponent.$mount({target: document.body})

the jsx part will be transpiled into:

const myComponent = new MyDiv({
  $data: {
    className: 'some class names',
    style: 'background-color: #CCC;'
  },
  children: [
    myHelloInstance,
    new MyTextInput({
      $methods: {
        input({value}) {
          myHelloInstance.$data.name = value
        }
    })
  ]
)

or with ef.createElement:

const myComponent = ef.createElement(MyDiv, {...propsForMyDiv}, myHelloInstance, ef.createElement(MyTextInput, {...propsForMyTextInput}))

If we map needed props to the component class it self, things could be more easy like:

const myComponent = <MyDiv className='some class names' style='background-color: #CCC;'>
  {myHelloInstance}
  <MyInput input={({value}) => {
    myHelloInstance.name = value
  }/>
</MyDiv>

Using the first implementation we should write our own transpilers, while with the second implementation, we could just use the existing jsx transpilers to have the transpiling done like babel and buble. Using the second approach could be more convenient when project bootstraps.

Typo in readme

SublimeEFMLHightlighter should be SublimeEFMLHighlighter

Add ability to distinguish `keyEvent.code`

https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
https://en.wikipedia.org/wiki/Scancode

Though we cannot get the scancode directly from browser, we can still access the similar information at keyEvent.code. For example, Enter and NumpadEnter both give out a key code of 13, but the keyEvent.code gives out differently with Enter and NumpadEnter. While Chrome and Firefox act differently on some keys according to MDN, supporting this feature still needs a well discussion or design.

Text fragment support

Now we have mounting points and list mounting points, which can handle all ef-components at ease. But times when people want only to put a line of text into the mounting points, things gets really hard. Maybe we can have text fragments supported, which can also be helpful when jsx(#3) gets supported.

For Example:

const {t} = ef

const component = new (t`
>div
  -nodeMountingPoint
  +listMountingPoint
`)()

component.nodeMountingPoint = 'Hello World!'
component.listMountingPoint.push('Hello ef.js!')
component.listMountingPoint.push('Hello text fragments!')

component.$mount({target: document.body})

could be rendered as:

<div>Hello World!Hello ef.js!Hello text fragments!</div>

while with jsx support, we can have:

const {t} = ef

const Div = t`
>div
  +children
`

const H1 = t`
>H1
  +children
`

const component = <Div>
  First line text
  <H1>Title text here</H1>
  Third line text
</Div>

component.$mount({target: document.body})

Mount option modification

before and after is sort of ambiguous, consider change to append and prepend while before and after could do other things.

Implement fragment

For now a component can only be created within a tag, thus is not really convenient for some complicated circumstances. For example, a template like:

>h1
  .Hello, World!
>h2
  .Hello, ef.js

could be rendered as:

<h1>Hello, World!</h1>
<h2>Hello, ef.js</h2>

This can also be helpful when using jsx implementation(#3), while React provides React.fragment which does exactly the same thing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.