Like I promised in my previous post, I spent more time in this project to create a usable frontend framework with a javascript view controller.
The main objective of this exercise is to create a low level view controller. This should be flexible and modular and programmable. If I write down a check list, it would look like this.
- No transpiling/template-rendering (custom syntaxes)
- No bundlers. Apps should be able to run straight in browser
- Use low level apis dom apis, create custom-elements with shadow-root and maintain isolation
The decision to keep the api minimal/low-level is so that it will be possible to use other existing legacy libraries while maintaining isolation. For example, even though promax
doesn't have a template system, one could bring their own template system and use within promax
This is the api I came up with:
index.html
. Herep-frame
is like iframe, but it loads another html file into a shadow dom.
<script src="./module/.bundle/script.js"></script>
<p-frame src="./src/app.html"></p-frame>
You can use this without the rest of promax too, just to embed any html with its own contained style and scoped scripts. (Limiting scope is what the rest of promax is about)
./src/app.html
<!-- UI -->
<button id="button" onclick="pscope.clickListener(event)">
This text will be replaced by initialState value
</button>
<!-- view controller -->
<script>
promax
.initState({ buttonText: "starting" })
.setRenderer(({ root, state }) => {
root.getElementById("button").innerHTML = state.buttonText;
})
.attachScope(({ getState, patchDom }) => {
let clickCount = 0;
return {
// returns scope object
clickListener: () => {
clickCount++;
patchDom({
buttonText: "Clicked " + clickCount,
});
},
};
});
</script>
This is a click counter button. At first rendering, button text is set to "starting". Then we attach an event listener to count clicks. Butten text updates to "Clicked #count" after each click
Breakdown of ./src/app.html
Notice the onclick=pscope.clickListener(event)
. pscope
is a global proxy that know how to look up the component scope object at run time. It only works for events with target
field. It will throw error if a promax scope is not defined.
<!-- UI -->
<button id="button" onclick="pscope.clickListener(event)">
This is the welcome page!! This is also an html file. :)
</button>
<!-- view controller -->
<script>
promax
is a global variable injected by p-frame
component.
Note that promax.initState().setRenderer().attachScope()
works only in this fixed order.
promax
initState
sets first state and creates a closure around it to attach renderer.
.initState(
{ buttonText: "starting" }
)
setRenderer
sets a renderer function. This is the only place you touch dom. asynchronous dom modifications are prohibited. This will also invoke renderer immediately after setting. This is also why we start withinitState
.setRenderer((
{root,state }
) => {
root.getElementById("button").innerHTML = state.buttonText;
})
attachScope
is where you inject values to component scope. Event listeners are defined through scope object. Currently this is the only use-case, but it has other uses.
.attachScope((
{ getState, patchDom }
) => {
let clickCount = 0;
return {
// returns scope object
clickListener: () => {
clickCount++;
patchDom({
buttonText: "Clicked " + clickCount
});
},
};
});
</script>
That is basically it for this exercise. The above code is a click counter button, if you didn't guess it already.
Next episode
I am debating myself on adding a prop system to pass javascript objects, or stick with html attributes. I really don't like to add custom syntax. Most likely to stick with attributes.
I haven't figured out how to render lists and conditional views yet. But the fact that we have a view controller is giving me great confidence.
I rencently renamed the project to
romax
. I know this is not an objectively better name than promax, but at least it sounds less lame to me.
Github: /bwowsersource/romax
To Do
- Props
- Dynamic rendering lists
- Conditional rendeing
- script scope
- promax.renderFromString and promax.renderFromTemplate
Top comments (0)