The following guide will help you get through the upgrade process quickly and smoothly. After any given step you should have a working application.
This means you should complete a step and get it merged back into master fairly quickly. You shouldn't need to have a marko-4-upgrade
branch for your project that lives in limbo for a couple of weeks falling behind the other changes that are being merged into master.
If you do decide to pause and later jump in where you left off, be sure to repeat Step 0 first 😉.
Run your application and tests to ensure your project is in a working state. There's little worse than finding an issue after you've started the upgrade process only to figure out the issue existed beforehand.
Before we start, you'll want to make sure that you are already on the latest 3.x
release of marko
and the latest 6.x
release of marko-widgets
. Later versions of marko@3
and marko-widgets@6
ship with deprecation warnings should be handled (the next step) before upgrading to Marko 4. This will make your life so much easier.
npm install marko@^3 marko-widgets@^6
or
yarn upgrade marko@^3 marko-widgets@^6
Note: Do NOT run
npm install marko
(without the@^3
). This will put you on Marko 4 and we're not quite there yet.
Run your application and tests and ensure that there are no deprecation warnings logged to the console. If there are, you should follow the instructions in the deprecation messages to avoid the deprecated pattern and migrate to the recommended pattern. In particular:
Using w-extend
is not supported by the compatibility layer in Marko 4, compose components instead (you'll need an extra wrapper node in Marko 3, but it can be removed once you upgrade).
// UNSUPPORTED<fancy-button w-extend/>// SUPPORTED<span w-bind><fancy-button/></span>
// UNSUPPORTEDfancy-button w-extend// SUPPORTEDspan w-bindfancy-button
Using <widget-types/>
to conditionally bind to two or more different widgets is not supported by the compatibility layer in Marko 4 (using <widget-types>
to disable binding is still supported).
// UNSUPPORTED<widget-types default="./widget" mobile="./widget-mobile"/><div w-bind=data.isMobile ? 'default' : 'mobile'>...</div>// SUPPORTED<widget-types default="./"/><div w-bind=data.includeWidget ? 'default' : null>...</div>
// UNSUPPORTEDwidget-types default="./widget" mobile="./widget-mobile"div w-bind=data.isMobile ? "default" : "mobile" -- ...// SUPPORTEDwidget-types default="./"div w-bind=data.includeWidget ? "default" : null -- ...
Using data-widget
and data-w-*
attributes in your application code and tests. These attributes existed so Marko could keep track of DOM nodes and they don't exist in Marko 4.
If you're accessing the DOM to get an Element, prefer next/prevElementSibling
, first/lastElementChild
and children
instead of , next/prevSibling
and first/lastChild
. There are differences in the DOM structure generated by Marko 3 vs 4 and you might not get the same node after upgrading with the non-element version of these properties.childNodes
In Marko 3, the rendering API was different between templates (which returned strings) and widgets (which returned a RenderResult). In Marko 4, all render methods return RenderResults, so if you need a string, use renderToString
which will still return a string after upgrading.
template.render(data)
without a callback → template.renderToString(data)
template.render(data, callback)
with a callback → template.renderToString(data, callback)
template.renderSync(data)
→ template.renderToString(data)
widget.render(data)
without a callback → widget.renderSync
Please note that you will need to deal with deprecations in any dependent modules as well before continuing with the upgrade process (usually this means updating dependencies).
Before upgrading to Marko 4, it is recommended to make sure that your Marko-related dependencies are up-to-date. Many packages have versions that support both Marko 3 and Marko 4. If one of your dependencies doesn't have a version that supports both, you'll need to wait to upgrade it until you're upgrading Marko.
After upgrading, run your application and tests to ensure that everything is still working as intended. If there are any issues, please refer to the changelogs of the modules you just upgraded to see if you need to make any changes within your app to accommodate the new versions.
If you have any Marko components installed from npm, chances are at least one of them has a direct dependency on marko@^3
or marko-widgets@^6
. This is bad.
Marko 4 for has legacy support for Marko 3 widgets, but if a dependency directly depends on an old version of marko
or marko-widgets
, it will try to use that old version and after your app is on Marko 4 this will cause all sorts of errors.
You can run npm ls marko marko-widgets
(or yarn list marko marko-widgets
) to view any dependencies that have a direct dependency on either of these. Any packages that provide components will need to move these into peerDependencies
.
NOTE: Some modules that have direct dependencies on Marko do not need to be updated, but as a general rule, they do.
Let's take a look at what a package.json
for a dependency should look like (minus the comments, because that's not valid JSON 😉).
// marko and marko-widgets are NOT here"dependencies":{}// use marko@3 and marko-widgets@6 for testing"devDependencies":"marko": "^3""marko-widgets": "^6"// use the app's version of marko/marko-widgets, but// give a warning if it doesn't match the versions this// package is compatible with (both Marko 3 and 4)"peerDependencies":"marko": "^3 || ^4""marko-widgets": "^6 || ^7"
yarn
resolutionsIf you don't maintain the package that needs to move marko
and/or marko-widgets
to peerDependencies
, you can make use of yarn's selective dependency resolutions. Add the following to your package.json
and it will force your dependencies to use the latest version of Marko:
"name": "appname""version": "0.0.0"/* ... */"resolutions":"**/marko": "^4""**/marko-widgets": "^7"
Phew! With all the prep out of the way we're finally ready to upgrade marko
!
npm install marko@^4 marko-widgets@^7
or
yarn upgrade marko@^4 marko-widgets@^7
If at this point you're thinking, "Wait... I thought Marko 4 didn't need marko-widgets
any more...", you'd be correct. marko-widgets@7
is just there to help with the migration. We'll remove it soon, but for now, there's still a bunch of calls to require('marko-widgets').defineComponent
all over your app's code and we don't want that to throw saying it can't find the module.
Now run your application and its tests. Marko 4 contains a legacy compatibility layer, so everything should still work! Congratulations, you've upgraded to Marko 4!
You will however have noticed a swarm of deprecations in your console. We'll get to those.
NOTE:
marko-widgets@7
isn't tagged as latest, thereforenpm install marko-widgets
andnpm install marko-widgets@latest
will NOT get you to7.x
.
Despite having a lot of deprecation warnings, the beauty is that you can deal with them on a template by template, component by component basis and keep a working app in between migrating each template/component.
Additionally, any deprecation warnings that start with MIGRATION
are automatically migratable by marko migrate
. Most migrations are 100% safe and will run automatically. However, there are a few migrations which are considered unsafe: they may only get you 90% of the way there. These migrations will prompt and ask if you want to run the migration. It is highly recommended to run these only on a single component at a time and then finish the migration manually using the guide below so that your app is always in a working state
<layout-use>
→ Layout components with <@tags>
(or import
)The layout taglib is no longer necessary in Marko 4 because components have the ability to easily recieve multiple blocks of content and can render those blocks whereever they like.
You can also directly import
a template by it's path much like <layout-use>
and render it using the <${dynamic}/>
syntax, but the recommended way to reference it is by creating components. You should move the layout into your components/
directory and use it as any other component.
Old:
src/components/layouts/site-layout.markopages/home/template.marko
<layout-use'../../layouts/site-layout.marko'><layout-put into="body">Hello World</layout-put></layout-use>
layout-use"../../layouts/site-layout.marko"layout-put into="body" -- Hello World
New (automatically migratable):
import<><@body>Hello World</@body></>
import<><@body>Hello World</@body></>
NOTE: If you're using a layout from an npm package that requires you to reference it by its path, you can
import
it. However we recommend checking to see if there is a newer version of the package that exposes the layout as a component or updating the package to expose the layout as a component.
New (Recommended):
src/components/site-layout.markopages/home/template.marko
<site-layout><@body>Hello World</@body></site-layout>
site-layout@body -- Hello World
Related Docs: Custom Tags
<layout-placeholder>
→ <${dynamic}>
Old:
<html><body><layout-placeholder name="body">Default body content</layout-placeholder></body></html>
htmlbodylayout-placeholder name="body" -- Default body content
New (automatically migratable):
<html><body><ifinput.body></></if><else>Default body content</else></body></html>
htmlbodyifinput.body</>else -- Default body content
Old:
<html><body><layout-placeholder name="body"/></body></html>
htmlbodylayout-placeholder name="body"
New (automatically migratable):
<html><body></></body></html>
htmlbody</>
Related Docs: Body content
<script marko-init>
→ import
/static
The <script marko-init>
attribute is deprecated, but in its place you get ES Module import
syntax and the static
keyword. Anything after the static
keyword is executed as JavaScript when the template is loaded.
Old:
<script marko-init>var capitalize = require('./util/caps');var NAME = capitalize('Frank');</script><div></div>
<script marko-init>var capitalize = require('./util/caps');var NAME = capitalize('Frank');</script>div --
New (automatically migratable):
importstatic<div></div>
importstaticdiv --
Related Docs:
<var>
/<assign>
/<invoke>
→ $
The <var>
tag is deprecated, but in its place you get $
. Similar to static
, a line that begins with $
will execute the JavaScript that follows as a part of each render.
Old:
<var name="Frank"/><assign name="John"/><invoke console.logname)/><div></div>
var name="Frank";assign name="John"invoke console.logname)div --
New (automatically migratable):
<div></div>
div --
Related Docs: Inline JavaScript
w-*
Atrributesw-id
→ key
Old:
<div w-id="foo"/>
div w-id="foo"
New:
<div key="foo"/>
div key="foo"
Related Docs: The key
attribute
w-for
→ for:scoped
Old:
<label w-for="name">Name</label><input type="text" w-id="name"/>
label w-for="name" -- Nameinput type="text" w-id="name"
New (automatically migratable):
<label for:scoped="name">Name</label><input type="text" id:scoped="name"/>
label for:scoped="name" -- Nameinput type="text" id:scoped="name"
Related Docs: The :scoped
attribute modifier
widget.elId
→ :scoped
You can use :scoped
on any attribute to reference a scoped value. This value will be unique to this component instance and is useful for other attributes that take an id
to reference, so you can use a scoped id
instead.
Old:
<button aria-describedby=widget.elId"tooltip">...</button><div w-id="tooltip" role="tooltip">...</div>
button aria-describedby=widget.elId"tooltip" -- ...div w-id="tooltip" role="tooltip" -- ...
New (automatically migratable):
<button aria-describedby:scoped="tooltip">...</button><div id:scoped="tooltip" role="tooltip">...</div>
button aria-describedby:scoped="tooltip" -- ...div id:scoped="tooltip" role="tooltip" -- ...
Related Docs: The :scoped
attribute modifier
w-preserve
→ no-update
Old:
<div w-preserve>...</div>
div w-preserve -- ...
New (automatically migratable):
<div no-update>...</div>
div no-update -- ...
w-preserve-attrs
→ :no-update
Old:
<div class="foo" w-preserve-attrs="class">...</div>
div class="foo" w-preserve-attrs="class" -- ...
New (automatically migratable):
<div class:no-update="foo">...</div>
div class:no-update="foo" -- ...
w-on-*
→ on-*()
Old:
<button w-on-click="handleClick">click me</button>
button w-on-click="handleClick" -- click me
or
<button w-onClick="handleClick">click me</button>
button w-onClick="handleClick" -- click me
New (automatically migratable):
<button on-click'handleClick')>click me</button>
button on-click"handleClick") -- click me
The new syntax support binding additional arguments.
Related Docs: Listening to events
It's time to migrate your first legacy (Marko 3 style) widget to a Marko 4 component. Before you continue, please note that you'll need to go through all these steps for a given component. Partially migrated components will break your app. This is the section for which there are unsafe migrations provided by marko migrate
. Again, these migrations should be run on a single component, and then follow the steps below to ensure the component is fully migrated.
w-bind
w-bind
is the indicator used by Marko to determine whether a component should operate in legacy mode. Marko 4 automatically binds the top level elements in a component, so w-bind
is not necessary. Let's remove it. There's no turning back now...
this.getWidget
→ this.getComponent
this.getWidgets
→ this.getComponents
Related Docs:
Old:
Marko 3 widgets were traditionally structured as follows:
components/my-cool-component/index.js → Widget Definitiontemplate.marko → Widget Marko Template
Your index.js
acts as the entry point for the component, contains a call to require('marko-widgets').defineComponent
and requires template.marko
.
New:
Marko 4 changes the filename structure and makes the template the entry point for the component:
template.marko
→ index.marko
(the template)index.js
→ component.js
(component behavior for server/client)Thus a full split file based component in Marko 4 would be structured as follows:
components/my-cool-component/component.js → Component Definitionindex.marko → Component Marko Template
Marko 4 also introduces single file components within index.marko
.
Old:
Marko 3 Split renderer/widgets were structured as follows:
components/my-cool-component/renderer.js → Renderer Definitiontemplate.marko → Widget Marko Templatewidget.js → Widget Definition
Your renderer.js
acts as the entry point for the component, contains a call to require('marko-widgets').defineRenderer
and requires template.marko
.
Your widget.js
should contain a call to require('marko-widgets').defineWidget
.
New:
Marko 4 changes the filename structure and makes the template the entry point for the component:
template.marko
→ index.marko
(the template)renderer.js
→ component.js
(component behavior for server/client)widget.js
→ component-browser.js
(component behavior for client only & causes component.js
to be server only).Thus a full component in Marko 4 would be structured as follows:
components/my-cool-component/component.js → Component Definition (Server)component-browser.js → Component Definition (Browser)index.marko → Component Marko Template
marko-widgets
Old:
As noted in file structure section above, Marko 3 used marko-widgets
to define a component within each index.js
. This was the entry point for the component and it required template.marko
so it knew how to render itself.
moduleexports =;
New:
In Marko 4, marko-widgets
is no longer necessary and index.marko
becomes the component entry point so referencing the template from the component.js
file is not necessary (and might cause circular dependency issues).
moduleexports =// ...;
NOTE: Once this step has been completed for all components in a project, you can remove
marko-widgets
as a dependency!
data
→ input
/state
Old:
In Marko 3, a template received a single data
variable that contained the input data. In the case of a widget, the getTemplateData
method could be used to combine the widget state with the widget input data into a single data
object to be passed to the template.
// ...{returnfoo: statefoobar: inputbar}// ...
<ul><li>Foo: </li><li>Bar: </li></ul>
ulli -- Foo:li -- Bar:
New:
In Marko 4, components are passed the input data as input
and a separate state
variable contains component state. This removes the need for getTemplateData
! (And it is no longer called)
<ul><li>Foo: </li><li>Bar: </li></ul>
ulli -- Foo:li -- Bar:
// getTemplateData is removed
NOTE:
input
is aliased asdata
, so accessingdata
will still work, but it is recommended to useinput
. Accessingdata
will be officially deprecated at a later date.
If your getTemplateData
has a lot of logic in it to transform the state
or input
, you'll probably want to retain that logic, but still remove the getTemplateData
method.
Old:
// ...getTemplateData: function(state, input) {var value = state.value;var sign;if (value < 0) {sign = 'negative';} else if (value > 0) {sign = 'positive';}return {value: value,sign: sign};}// ...
<div class=data.sign></div>
div class=data.sign --
New:
Instead of manipulating input
/state
before it makes it to the template, move the manipulation logic from getTemplateData
into a helper function that can be imported into your template. This has the added benefit that it is now easy to write unit tests for any helper functions you might have.
exports.getSign = function(value) {var sign;if (value < 0) {sign = 'negative';} else if (value > 0) {sign = 'positive';}return sign;}
import<div class=getSignstate.value></div>
importdiv class=getSignstate.value --
Old:
In Marko 3, input
was transient: it was only there for the first render (or when new input was passed into a component). This meant that when your component re-rendered, if there was data that was in the input
that was necessary for a re-render, you had to put it in state
to make sure it got kept around.
// ...{returncount: inputinitialCount || 0color: inputcolor}// ...
<div style={ color:data.color }></div>
div style={color: data.color} --
New:
In Marko 4, getInitialState
is no longer called. You can set initial state in onCreate
. If you have some state that is derived from input
and should be reset when the input
changes, you can set it in onInput
, but this should be a rare occurrence.
Marko 4 keeps the original input
around for subsequent renders, so you don't need to add input
properties into the state
. Only values that are controlled by the component should be put in state
.
// ...{thisstate =count: inputinitialCount || 0;}// ...
<div style={ color:input.color }></div>
div style={color: input.color} --
NOTE: From within the component you can access
this.state
as well asthis.input
Related Docs:
Old:
In Marko 3, the init
method was used to set things up in the browser and was the first time the DOM for the component was ready since init
was called immediately after mounting the component to the DOM.
The getWidgetConfig
method was used to create a config
object that would be serialized and sent to the browser to be used in the init
method. This was necessary because input
was not available in init
.
// ...{;}{returnpaginate: inputpaginatescrollY: inputscrollY;}// ...
New:
init
has been renamed to the more appropriate onMount
which better describes where in a component's lifecycle it is called. getWidgetConfig
is no longer necessary (or called) because we can access this.input
.
// ...{;}// ...
NOTE: If you need values from
out
, you can grab them inonCreate
, attach them to the component instance and access them inonMount
://...{thisvalue = outglobalvalue;}{console}// ...
Related Docs: onMount
Old:
// ...{return inputrenderBody || inputlabel}//
<button><w-body/></button>
buttonw-body
New:
<button><ifinput.renderBody></></if><else></else></button>
buttonifinput.renderBody</>else --
getInitialProps(input, out)
Old:
This method existed because input
was passed to getInitialState
, getInitialBody
, getWidgetConfig
and getTemplateData
. If the input needed to be transformed, getInitialProps
allowed you to do it in a single place.
New:
getInitialProps
is no longer called. If you need to transform your input, move that logic into helper methods or another appropriate location.
A few of these have already been covered:
init
➔ onMount
getWidgetConfig
➔ onMount
/onCreate
getInitialState
➔ onCreate
getTemplateData
➔ (no longer needed)getInitialProps
➔ (no longer needed)getInitialBody
➔ (no longer needed)onRender
The legacy onRender
method was called with firstRender === true
immediately after mounting the widget in the DOM.
Subsequent calls onRender
occurred immediately after calls to onUpdate
.
This behavior did not align with where the actual render takes place (it actually occurs before mounting and before updating the DOM). So we've changed its behavior in Marko 4. If you were using onRender
in Marko 3, use onMount
or onUpdate
instead.
onRender
(first render) ➔ onMount
onRender
(subsequent renders) ➔ onUpdate
onBeforeUpdate
and onUpdate
onBeforeUpdate
➔ onUpdate
/onRender
onUpdate
➔ onUpdate
The onUpdate
is called after DOM updates have been made. The onRender
method is now called before rendering, so it can replace some use-cases of onBeforeUpdate
.
onBeforeDestroy
and onDestroy
onBeforeDestroy
➔ onDestroy
onDestroy
➔ onDestroy
The onDestroy
is now called immediately before destroying the DOM associated with a component.
See how the legacy adaptor remaps these methods.
Related Docs: Lifecycle events
👍🎉 You've fully migrated your first component! 🎉👍
Repeat this process for each component in your app. As you get familiar with "Thinking in Marko 4" each one will be easier. And remember, you should have a working application after converting each individual component, so you don't have to do it all at once.
EDITHelpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.