8 min. reading time

Beside the Xtext framework which has made language engineering easy as a known tool for developing Domain Specific Languages (DSL) in the Java/Eclipse ecosystem, there is the web platform and the desire to practice DSL use cases within the JavaScript client frameworks. In this article, I am going to demonstrate how to use ReactJS to write a dedicated web editor for designing and editing a DSL. This part 1 will have follow-up parts.

Knowing ReactJS for the sake of DSL needs

ReactJS is a JavaScript library written by Facebook for rendering UI components in the browser in a way that makes big amount of data re-rendered consistently with respect to user interactions or in general state mutations. In Xtext web to name one, the client is used as tool to view and interact with the back-end that does all the jobs and send responses back via Rest/Soap web-services or web-sockets.

Differently, I will implement the DSL features here as a fully client-side Single Page Application (SPA) with React.

In SPAs, many JS libraries unlike ReactJS have been using the pattern Model View Controller (MVC) for defining the three layers. For a use case like a web editor for models with high density of domain vocabularies mostly rendered as text, the MVC is an over-engineering path to go for me. ReactJS made that much more simple. It defines three aspects with which I will build the DSL editor:

  1. stateful / stateless components
  2. global-state
  3. state-reducers


UI is defined as a tree of components usually with a stateful root component. The global state is the data to be rendered as well as the UI data. It is to be managed by the root component and passed down to the child ones. The state can be transformed from one snapshot to another by calling a reducer function. Every performed action (e.g. user-click) on the browser calls a suitable reducer function and once the global-state is mutated, ReactJS re-renders affected UI elements automatically and consistently.

React aspects through Xtext developer glasses

In this part, I am going to map the three ReactJS aspects above roughly to the following Xtext concepts simply enough so that we have a common understanding.

  1. Components: 
    Root component corresponds to Xtext ResourceSet
    Child components correspond to the children elements of the Xtext ResourceSet, i.e. Resources. So, the tree built starting of the ResourceSet until the leaf nodes in an Xtext model instance corresponds to a tree built as a global state in the React App.
  2. Global-state
    Corresponds to the whole Xtext model plus the UI metadata like which model element is highlighted etc
  3. State-reducers
    Corresponds to different eclipse UI tasks like editing, navigating, outline etc

Example 1: Hello world

You define what a component does look like:

ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.body
)

In this case, React renders a h1 element in the body of the html document. We can define some data in JSON format to be rendered and split the app into two components:
const data = [['Hello', ',', 'world', '!'],['A', 'new', 'line']]
function Line(props) {
    return <div>{props.words.map(w=><span>{w}{' '}</span>)}</div>
}
function App(props) {
    const lines=props.data.map(l=>Line words={l}/>)
    return <div>{lines}</div> 
}
ReactDOM.render(<App data={data}/>, document.getElementById('root'))


The result: 

hello-world-react

Example 2: Domain specific


Let's make the input domain specific. For example a Bookshop DSL.

const data = [
    {rule: 'Book', name: 'Clean Code', author: 'Robert Cecil Martin'},
    {rule: 'Book', name: 'Learning React', author: 'Alex Banks, Eve Porcello'},
]


And a nice dedicated web viewer with some CSS would result this:

css-react

But if we use our React App function above so far to achieve that, we would have to re-write the input into:

const data = [
    [{classes:'keyword', word:'book'},
        {classes:'word', word:'Clean Code'},
        {classes:'keyword', word:'by'},
        {classes:'word', word:'Robert Cecil Martin'}],
    [{classes:'keyword', word:'book'},
        {classes:'word', word:'Learning React'},
        {classes:'keyword', word:'by'},
        {classes:'word', word:'Alex Banks, Eve Porcello'}]]

Here every statement in the browser corresponds to an array of objects. Every object represents a word. In the word objects, UI information is given e.g. “keyword” to enable the React Line component add different CSS classes. So, the Line function will look like:

function Line(props) {
    return <div>
	{props.words.map(w=><span className={w.classes}>{w}{' '}</span>)}
</div> } //CSS .keyword { color: #D2691E; font-weight: bold; }


Something to improve?

Yes of course. The duplicated contents can be extracted in a separate data structure. So, the final renderable input to the App function would be constructed from both the pure data and the schema or grammar data.

Define grammar

const grammar = [
    {name: 'Book',
     identifiable: true,
     contents: [
            {keyword: 'book', id: 'name'},
            {keyword: 'by', input: 'author'}]}


// transform into renderable data structure
const lines = prepareContents(grammar, initialData, this.onRefClick)

prepareContents is a javascript function that maps data into the renderable data structure based on the grammar. It might also get a list of function references to handle different actions that are triggered by the user in the browser. For brevity, I will show only the action (onRefClick) which means, a user click on a cross-reference to highlight the referenced model element.

Identifiable Model Elements

Like in Xtext framework, uniqueness of model elements is achieved by using „name“ property for grammar rules. Here is no exception. We do it like a charm, in our grammar above, you noticed in identifiable: true and {keyword: 'book', id: 'name'}. With that I am telling: 

  • The rule Book is an identifiable one and this is done conventionally by providing an “id”. The Line will be removed from the global-state in case it is identifiable and has no id given.
  • A word declaration consists of a fixed keyword 'book' and then comes the name which is set as the id all over the whole state of the React App.

Adding Cross-Reference

Likewise I will extend the Book grammar rule to enable adding cross-references among books. {keyword: 'recommends', ref: {id: 'recommends', type: 'Book'}}.

So, I extended the Book rule so that the user can declare book recommendations. Syntactically, by writing the keyword 'recommends' then a name of another declared book. For simplicity, I will assume that a book can recommend 0 or 1 book and it may recommend itself.

You can give a simple guess what a sample data would look like now.

{rule: 'Book', 
	name: 'AngularJS in Action', author: 'Brian Ford & Lukas Ruebbelke', 
	recommends: 'Learning React'}
}


So, in the data, the book „AngularJS in Action“ recommends „Learning React“. The editor would render our above data like this:

Editor-React


Plenty of UI features can be gained by just updating the state of the app. The root component will pass the updated state down to its child components and React will re-render accordingly. Simply enough, to make the referenced book highlighted, all I do is to implement the above onRefClick function in the root component such that, the referenced state element is searched and once found, it gets a CSS class which in turn causes the line to be highlighted in the browser.

 
onRefClick(e, text, rule) {
    e.stopPropagation()
    const newLines = this.state.lines.map(line=> {
        if (line['rule'] !== rule) {
            return line
        }
        const idWord = line.words.find(w=> {
            return w['type'] == 'id'
        })
        if (idWord['text'].toLowerCase() == text.toLowerCase()) {
            line['classes'] = this.addClasses(line, 'selected')
            return line
        }
        return line
    })
    this.setState({lines: newLines})
}


Once this.setState({lines: newLines}) is called meaning a new state is given to the root component, everything else will happen by React based on how we designed our components. The programming effort went into the function prepareContents(grammar, data, onRefClick).

Let Users Define Grammar and Enter Data

In the next part, I will provide a link to the App to try it online but for that, I would extend it to allow entering the grammar by the user and type models based on.

Comments