diff --git a/404.html b/404.html index 68df705b5..15cc47720 100644 --- a/404.html +++ b/404.html @@ -4,13 +4,13 @@ Page Not Found | IMA.js - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/advanced-features/dynamic-imports/index.html b/advanced-features/dynamic-imports/index.html index 766919b79..363d60a6d 100644 --- a/advanced-features/dynamic-imports/index.html +++ b/advanced-features/dynamic-imports/index.html @@ -4,13 +4,13 @@ Dynamic imports | IMA.js - +
-
Skip to main content

Dynamic imports

Dynamic imports

Preloading and prefetching

Since we're using webpack, to built the application, it already has support for inline directives for preloading and prefetching. Using this comment:

import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

will result in<link rel="prefetch" href="login-modal-chunk.js"> being appended in the head of the page. For more information about

React suspense

Suspense currently doesn't support SSR. However you can use it to load client-side react components. Don't forget to add proper handlers so it only gets rendered on client, since SSR will result in an hydratation error.

- +
Skip to main content

Dynamic imports

Dynamic imports

Preloading and prefetching

Since we're using webpack, to built the application, it already has support for inline directives for preloading and prefetching. Using this comment:

import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

will result in<link rel="prefetch" href="login-modal-chunk.js"> being appended in the head of the page. For more information about

React suspense

Suspense currently doesn't support SSR. However you can use it to load client-side react components. Don't forget to add proper handlers so it only gets rendered on client, since SSR will result in an hydratation error.

+ \ No newline at end of file diff --git a/assets/js/046a2c8d.3540c591.js b/assets/js/046a2c8d.41ff76de.js similarity index 99% rename from assets/js/046a2c8d.3540c591.js rename to assets/js/046a2c8d.41ff76de.js index 6e236f892..7647e6c9a 100644 --- a/assets/js/046a2c8d.3540c591.js +++ b/assets/js/046a2c8d.41ff76de.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[3685],{5680:(e,t,n)=>{n.d(t,{xA:()=>c,yg:()=>m});var a=n(6540);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function s(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=a.createContext({}),p=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):s(s({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(l.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},g=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,c=o(e,["components","mdxType","originalType","parentName"]),u=p(n),g=r,m=u["".concat(l,".").concat(g)]||u[g]||d[g]||i;return n?a.createElement(m,s(s({ref:t},c),{},{components:n})):a.createElement(m,s({ref:t},c))}));function m(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,s=new Array(i);s[0]=g;var o={};for(var l in t)hasOwnProperty.call(t,l)&&(o[l]=t[l]);o.originalType=e,o[u]="string"==typeof e?e:r,s[1]=o;for(var p=2;p{n.d(t,{A:()=>s});var a=n(6540),r=n(8017);const i={tabItem:"tabItem_Ymn6"};function s(e){let{children:t,hidden:n,className:s}=e;return a.createElement("div",{role:"tabpanel",className:(0,r.A)(i.tabItem,s),hidden:n},t)}},1253:(e,t,n)=>{n.d(t,{A:()=>C});var a=n(8102),r=n(6540),i=n(8017),s=n(3104),o=n(9519),l=n(7485),p=n(1682),c=n(9466);function u(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:a,default:r}}=e;return{value:t,label:n,attributes:a,default:r}}))}function d(e){const{values:t,children:n}=e;return(0,r.useMemo)((()=>{const e=t??u(n);return function(e){const t=(0,p.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function g(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function m(e){let{queryString:t=!1,groupId:n}=e;const a=(0,o.W6)(),i=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,l.aZ)(i),(0,r.useCallback)((e=>{if(!i)return;const t=new URLSearchParams(a.location.search);t.set(i,e),a.replace({...a.location,search:t.toString()})}),[i,a])]}function y(e){const{defaultValue:t,queryString:n=!1,groupId:a}=e,i=d(e),[s,o]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!g({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const a=n.find((e=>e.default))??n[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:t,tabValues:i}))),[l,p]=m({queryString:n,groupId:a}),[u,y]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,i]=(0,c.Dv)(n);return[a,(0,r.useCallback)((e=>{n&&i.set(e)}),[n,i])]}({groupId:a}),f=(()=>{const e=l??u;return g({value:e,tabValues:i})?e:null})();(0,r.useLayoutEffect)((()=>{f&&o(f)}),[f]);return{selectedValue:s,selectValue:(0,r.useCallback)((e=>{if(!g({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),p(e),y(e)}),[p,y,i]),tabValues:i}}var f=n(2303);const h={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function b(e){let{className:t,block:n,selectedValue:o,selectValue:l,tabValues:p}=e;const c=[],{blockElementScrollPositionUntilNextRender:u}=(0,s.a_)(),d=e=>{const t=e.currentTarget,n=c.indexOf(t),a=p[n].value;a!==o&&(u(t),l(a))},g=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const n=c.indexOf(e.currentTarget)+1;t=c[n]??c[0];break}case"ArrowLeft":{const n=c.indexOf(e.currentTarget)-1;t=c[n]??c[c.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},t)},p.map((e=>{let{value:t,label:n,attributes:s}=e;return r.createElement("li",(0,a.A)({role:"tab",tabIndex:o===t?0:-1,"aria-selected":o===t,key:t,ref:e=>c.push(e),onKeyDown:g,onClick:d},s,{className:(0,i.A)("tabs__item",h.tabItem,s?.className,{"tabs__item--active":o===t})}),n??t)})))}function v(e){let{lazy:t,children:n,selectedValue:a}=e;const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(t){const e=i.find((e=>e.props.value===a));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},i.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==a}))))}function N(e){const t=y(e);return r.createElement("div",{className:(0,i.A)("tabs-container",h.tabList)},r.createElement(b,(0,a.A)({},e,t)),r.createElement(v,(0,a.A)({},e,t)))}function C(e){const t=(0,f.A)();return r.createElement(N,(0,a.A)({key:String(t)},e))}},7648:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>m,frontMatter:()=>o,metadata:()=>p,toc:()=>u});var a=n(8102),r=(n(6540),n(5680)),i=n(1253),s=n(6185);const o={title:"TypeScript",description:"Basic features > TypeScript"},l=void 0,p={unversionedId:"basic-features/typescript",id:"basic-features/typescript",title:"TypeScript",description:"Basic features > TypeScript",source:"@site/../docs/basic-features/typescript.md",sourceDirName:"basic-features",slug:"/basic-features/typescript",permalink:"/basic-features/typescript",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/typescript.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"TypeScript",description:"Basic features > TypeScript"},sidebar:"docs",previous:{title:"Error Handling",permalink:"/basic-features/error-handling"},next:{title:"Testing",permalink:"/basic-features/testing"}},c={},u=[{value:"tsconfig.json",id:"tsconfigjson",level:2},{value:"ima-env.d.ts",id:"ima-envdts",level:3},{value:"create-ima-app support",id:"create-ima-app-support",level:2},{value:"Controller generic types",id:"controller-generic-types",level:2},{value:"Extending existing interfaces",id:"extending-existing-interfaces",level:2},{value:"Extending Utils",id:"extending-utils",level:3},{value:"Extending ObjectContainer",id:"extending-objectcontainer",level:3},{value:"Extending Settings",id:"extending-settings",level:3},{value:"Dictionary localization keys",id:"dictionary-localization-keys",level:2}],d={toc:u},g="wrapper";function m(e){let{components:t,...n}=e;return(0,r.yg)(g,(0,a.A)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Since IMA.js v18 we provide ",(0,r.yg)("strong",{parentName:"p"},"support for Typescript in your application code")," with proper type declarations from the core packages."),(0,r.yg)("p",null,"To enable TypeScript in your project, first you need to add ",(0,r.yg)("inlineCode",{parentName:"p"},"typescript")," to your app dependencies:"),(0,r.yg)(i.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(s.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npm i -D typescript\n"))),(0,r.yg)(s.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add --dev typescript\n"))),(0,r.yg)(s.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add -D typescript\n")))),(0,r.yg)("h2",{id:"tsconfigjson"},"tsconfig.json"),(0,r.yg)("p",null,"Now create ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," file (that may look something like this):"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-json",metastring:"title=./tsconfig.json",title:"./tsconfig.json"},'{\n "compilerOptions": {\n "allowJs": true,\n "target": "ES2022",\n "lib": [\n "ES2022",\n "DOM",\n "DOM.Iterable"\n ],\n "module": "ES2022",\n "moduleResolution": "Node16",\n "strict": true,\n "resolveJsonModule": true,\n "jsx": "react-jsx",\n "baseUrl": ".",\n "outDir": "./build/ts-cache",\n "paths": {\n "app/*": [\n "app/*"\n ],\n }\n },\n "include": ["./app/**/*", "./build/tmp/types/**/*"],\n "exclude": ["./**/__tests__"]\n}\n')),(0,r.yg)("p",null,"When CLI detects existence of the ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," file, it automatically starts ",(0,r.yg)("strong",{parentName:"p"},"type checking")," and ",(0,r.yg)("strong",{parentName:"p"},"compiling")," files with ",(0,r.yg)("inlineCode",{parentName:"p"},"*.ts")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"*.tsx")," extensions."),(0,r.yg)("p",null,"Keep in mind that the code is still compiled using ",(0,r.yg)("a",{parentName:"p",href:"https://swc.rs/"},"swc"),", the same way JS code is. This means that certain settings in ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," only applies to type checking (like ",(0,r.yg)("inlineCode",{parentName:"p"},"target"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"moduleResolution"),", etc.), but compilation uses it's own settings to match the JS code."),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"You will also probably need to install additional ",(0,r.yg)("inlineCode",{parentName:"p"},"@types/*")," type definition libs to ensure proper support, like react types:"),(0,r.yg)(i.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(s.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npm i -D @types/react @types/react-dom\n"))),(0,r.yg)(s.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add --dev @types/react @types/react-dom\n"))),(0,r.yg)(s.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add -D @types/react @types/react-dom\n"))))),(0,r.yg)("h3",{id:"ima-envdts"},"ima-env.d.ts"),(0,r.yg)("p",null,"Additionally we recommend creating a new ",(0,r.yg)("inlineCode",{parentName:"p"},"ima-env.d.ts")," file in root of your ",(0,r.yg)("inlineCode",{parentName:"p"},"./app")," folder with following contents:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/ima-env.d.ts",title:"./app/ima-env.d.ts"},'/// \n')),(0,r.yg)("p",null,"This adds proper types support for webpack specific imports like images and other files."),(0,r.yg)("h2",{id:"create-ima-app-support"},(0,r.yg)("inlineCode",{parentName:"h2"},"create-ima-app")," support"),(0,r.yg)("p",null,"You can also easily create a typescript base IMA.js application using ",(0,r.yg)("inlineCode",{parentName:"p"},"--typescript")," cli argument when running ",(0,r.yg)("inlineCode",{parentName:"p"},"create-ima-app")," command:"),(0,r.yg)(i.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(s.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx create-ima-app ~/Desktop/ima-ts --typescript\n"))),(0,r.yg)(s.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx create-ima-app ~/Desktop/ima-ts --typescript\n"))),(0,r.yg)(s.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx create-ima-app ~/Desktop/ima-ts --typescript\n")))),(0,r.yg)("h2",{id:"controller-generic-types"},"Controller generic types"),(0,r.yg)("p",null,"The ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractController")," class follows similar principles used in React ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractComponent")," type. There are 3 generic types you can define on the class definition itself."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=AbstractController.ts",title:"AbstractController.ts"},"export class AbstractController<\n S extends PageState = {},\n R extends RouteParams = {},\n SS extends S = S\n> extends Controller;\n")),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("inlineCode",{parentName:"li"},"S")," - Use to define shape of your controller managed state."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("inlineCode",{parentName:"li"},"R")," - Use to define controller's route route params that are extracted to ",(0,r.yg)("inlineCode",{parentName:"li"},"this.params"),"."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("inlineCode",{parentName:"li"},"SS")," - Defaults to ",(0,r.yg)("inlineCode",{parentName:"li"},"S"),", however when you are using any extensions in your controller, that have their own state, you can merge those state types in this generic value, to have proper type support for ",(0,r.yg)("inlineCode",{parentName:"li"},"this.getState()")," method (this will now include all state keys, including ones used in extensions).")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=HomeController.ts",title:"HomeController.ts"},"import { TestExtension, GalleryExtensionState } from './GalleryExtension';\n\nexport type HomeControllerState = {\n cards: Promise;\n message: string;\n name: string;\n};\n\nexport class HomeController extends AbstractController<\n HomeControllerState,\n { detailId?: string },\n HomeControllerState & GalleryExtensionState\n>{\n static $extensions?: Dependencies> = [GalleryExtension];\n\n load(): HomeControllerState {\n const cardsPromise = this.#httpAgent\n .get('http://localhost:3001/static/static/public/cards.json')\n .then(response => response.body);\n\n // `state` contains all merged types from `SS` generic value.\n const state = this.getState();\n\n return {\n message: 'test',\n cards: cardsPromise,\n name: 'nam',\n };\n }\n}\n")),(0,r.yg)("h2",{id:"extending-existing-interfaces"},"Extending existing interfaces"),(0,r.yg)("p",null,"Since you can extend certain features like ",(0,r.yg)("inlineCode",{parentName:"p"},"ComponentUtils")," or settings from within your application or through plugins, and in order to provide type checking for these, we are using specific interfaces that you can extend using ",(0,r.yg)("a",{parentName:"p",href:"https://www.typescriptlang.org/docs/handbook/declaration-merging.html"},"Declaration Merging")," feature."),(0,r.yg)("p",null,"This ensures (when used correctly), that you always have correct static types when using these interfaces, even when they are extended in multiple places."),(0,r.yg)("h3",{id:"extending-utils"},"Extending ",(0,r.yg)("inlineCode",{parentName:"h3"},"Utils")),(0,r.yg)("p",null,"When using component utils, in addition to registering your classes using ",(0,r.yg)("inlineCode",{parentName:"p"},"ComponentUtils")," helper, make sure to also extend ",(0,r.yg)("inlineCode",{parentName:"p"},"Utils")," interface. This adds autocomplete and typechecking to ",(0,r.yg)("inlineCode",{parentName:"p"},"this.utils()")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"useComponentUtils")," in your components."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/config/bind.ts",title:"./app/config/bind.ts"},"declare module '@ima/core' {\n interface Utils {\n $CssClasses: typeof defaultCssClasses;\n }\n}\n\nexport const initBindApp: InitBindFunction = (ns, oc) => {\n oc.get(ComponentUtils).register({\n $CssClasses: '$CssClasses',\n });\n};\n")),(0,r.yg)("h3",{id:"extending-objectcontainer"},"Extending ",(0,r.yg)("inlineCode",{parentName:"h3"},"ObjectContainer")),(0,r.yg)("p",null,"Same goes for defining string aliases in Object container. This adds proper type checking to dependencies definition and ",(0,r.yg)("inlineCode",{parentName:"p"},"oc.get")," autocomplete."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/config/bind.ts",title:"./app/config/bind.ts"},"declare module '@ima/core' {\n interface OCAliasMap {\n $CssClasses: () => typeof cssClassNameProcessor;\n $PageRendererFactory: PageRendererFactory;\n API_KEY: string;\n }\n}\n\nexport const initBindApp: InitBindFunction = (ns, oc) => {\n oc.bind('$CssClasses', function () { return cssClassNameProcessor; });\n oc.bind('$PageRendererFactory', PageRendererFactory);\n oc.constant('API_KEY', '14fasdf');\n};\n")),(0,r.yg)("h3",{id:"extending-settings"},"Extending ",(0,r.yg)("inlineCode",{parentName:"h3"},"Settings")),(0,r.yg)("p",null,"This makes sure you don't have any missing or additional fields in your app settings. Other environments than ",(0,r.yg)("inlineCode",{parentName:"p"},"prod")," have all fields made optional, since they are deeply merged with the ",(0,r.yg)("inlineCode",{parentName:"p"},"prod")," settings."),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"Use ",(0,r.yg)("inlineCode",{parentName:"p"},"?:")," for settings with default values. This applies mostly to plugins.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/config/settings.ts",title:"./app/config/settings.ts"},"declare module '@ima/core' {\n interface Settings {\n links: Record<'documentation' | 'tutorial' | 'plugins' | 'api', string>;\n }\n}\n\nexport const initSettings: InitSettingsFunction = (ns, oc, config) => {\n return {\n prod: {\n links: {\n documentation: 'https://imajs.io/docs',\n api: 'https://imajs.io/api',\n },\n }\n }\n}\n\n")),(0,r.yg)("h2",{id:"dictionary-localization-keys"},"Dictionary localization keys"),(0,r.yg)("p",null,"When compiling app language files, we also generate dictionary keys during runtime. These are then stored in ",(0,r.yg)("inlineCode",{parentName:"p"},"'./build/tmp/types/dictionary.ts'")," file. Don't forget to include this file in ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," source files array, to have correct static type checking:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-json",metastring:"title=./tsconfig.json",title:"./tsconfig.json"},'{\n "include": ["./app/**/*", "./build/tmp/types/**/*"],\n}\n')),(0,r.yg)("admonition",{type:"note"},(0,r.yg)("p",{parentName:"admonition"},"When used in IMA.js plugins, you can manually extend the ",(0,r.yg)("inlineCode",{parentName:"p"},"DictionaryMap")," interface:"),(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-ts"},"declare module '@ima/core' {\n interface DictionaryMap {\n 'home.intro': string;\n }\n}\n\nexport {};\n"))))}m.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[3685],{5680:(e,t,n)=>{n.d(t,{xA:()=>c,yg:()=>m});var a=n(6540);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function s(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=a.createContext({}),p=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):s(s({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(l.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},g=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,c=o(e,["components","mdxType","originalType","parentName"]),u=p(n),g=r,m=u["".concat(l,".").concat(g)]||u[g]||d[g]||i;return n?a.createElement(m,s(s({ref:t},c),{},{components:n})):a.createElement(m,s({ref:t},c))}));function m(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,s=new Array(i);s[0]=g;var o={};for(var l in t)hasOwnProperty.call(t,l)&&(o[l]=t[l]);o.originalType=e,o[u]="string"==typeof e?e:r,s[1]=o;for(var p=2;p{n.d(t,{A:()=>s});var a=n(6540),r=n(8017);const i={tabItem:"tabItem_Ymn6"};function s(e){let{children:t,hidden:n,className:s}=e;return a.createElement("div",{role:"tabpanel",className:(0,r.A)(i.tabItem,s),hidden:n},t)}},1253:(e,t,n)=>{n.d(t,{A:()=>C});var a=n(8102),r=n(6540),i=n(8017),s=n(3104),o=n(9519),l=n(7485),p=n(1682),c=n(9466);function u(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:a,default:r}}=e;return{value:t,label:n,attributes:a,default:r}}))}function d(e){const{values:t,children:n}=e;return(0,r.useMemo)((()=>{const e=t??u(n);return function(e){const t=(0,p.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function g(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function m(e){let{queryString:t=!1,groupId:n}=e;const a=(0,o.W6)(),i=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,l.aZ)(i),(0,r.useCallback)((e=>{if(!i)return;const t=new URLSearchParams(a.location.search);t.set(i,e),a.replace({...a.location,search:t.toString()})}),[i,a])]}function y(e){const{defaultValue:t,queryString:n=!1,groupId:a}=e,i=d(e),[s,o]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!g({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const a=n.find((e=>e.default))??n[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:t,tabValues:i}))),[l,p]=m({queryString:n,groupId:a}),[u,y]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,i]=(0,c.Dv)(n);return[a,(0,r.useCallback)((e=>{n&&i.set(e)}),[n,i])]}({groupId:a}),f=(()=>{const e=l??u;return g({value:e,tabValues:i})?e:null})();(0,r.useLayoutEffect)((()=>{f&&o(f)}),[f]);return{selectedValue:s,selectValue:(0,r.useCallback)((e=>{if(!g({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),p(e),y(e)}),[p,y,i]),tabValues:i}}var f=n(2303);const h={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function b(e){let{className:t,block:n,selectedValue:o,selectValue:l,tabValues:p}=e;const c=[],{blockElementScrollPositionUntilNextRender:u}=(0,s.a_)(),d=e=>{const t=e.currentTarget,n=c.indexOf(t),a=p[n].value;a!==o&&(u(t),l(a))},g=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const n=c.indexOf(e.currentTarget)+1;t=c[n]??c[0];break}case"ArrowLeft":{const n=c.indexOf(e.currentTarget)-1;t=c[n]??c[c.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},t)},p.map((e=>{let{value:t,label:n,attributes:s}=e;return r.createElement("li",(0,a.A)({role:"tab",tabIndex:o===t?0:-1,"aria-selected":o===t,key:t,ref:e=>c.push(e),onKeyDown:g,onClick:d},s,{className:(0,i.A)("tabs__item",h.tabItem,s?.className,{"tabs__item--active":o===t})}),n??t)})))}function v(e){let{lazy:t,children:n,selectedValue:a}=e;const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(t){const e=i.find((e=>e.props.value===a));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},i.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==a}))))}function N(e){const t=y(e);return r.createElement("div",{className:(0,i.A)("tabs-container",h.tabList)},r.createElement(b,(0,a.A)({},e,t)),r.createElement(v,(0,a.A)({},e,t)))}function C(e){const t=(0,f.A)();return r.createElement(N,(0,a.A)({key:String(t)},e))}},7648:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>m,frontMatter:()=>o,metadata:()=>p,toc:()=>u});var a=n(8102),r=(n(6540),n(5680)),i=n(1253),s=n(6185);const o={title:"TypeScript",description:"Basic features > TypeScript"},l=void 0,p={unversionedId:"basic-features/typescript",id:"basic-features/typescript",title:"TypeScript",description:"Basic features > TypeScript",source:"@site/../docs/basic-features/typescript.md",sourceDirName:"basic-features",slug:"/basic-features/typescript",permalink:"/basic-features/typescript",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/typescript.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"TypeScript",description:"Basic features > TypeScript"},sidebar:"docs",previous:{title:"Error Handling",permalink:"/basic-features/error-handling"},next:{title:"Testing",permalink:"/basic-features/testing"}},c={},u=[{value:"tsconfig.json",id:"tsconfigjson",level:2},{value:"ima-env.d.ts",id:"ima-envdts",level:3},{value:"create-ima-app support",id:"create-ima-app-support",level:2},{value:"Controller generic types",id:"controller-generic-types",level:2},{value:"Extending existing interfaces",id:"extending-existing-interfaces",level:2},{value:"Extending Utils",id:"extending-utils",level:3},{value:"Extending ObjectContainer",id:"extending-objectcontainer",level:3},{value:"Extending Settings",id:"extending-settings",level:3},{value:"Dictionary localization keys",id:"dictionary-localization-keys",level:2}],d={toc:u},g="wrapper";function m(e){let{components:t,...n}=e;return(0,r.yg)(g,(0,a.A)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Since IMA.js v18 we provide ",(0,r.yg)("strong",{parentName:"p"},"support for Typescript in your application code")," with proper type declarations from the core packages."),(0,r.yg)("p",null,"To enable TypeScript in your project, first you need to add ",(0,r.yg)("inlineCode",{parentName:"p"},"typescript")," to your app dependencies:"),(0,r.yg)(i.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(s.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npm i -D typescript\n"))),(0,r.yg)(s.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add --dev typescript\n"))),(0,r.yg)(s.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add -D typescript\n")))),(0,r.yg)("h2",{id:"tsconfigjson"},"tsconfig.json"),(0,r.yg)("p",null,"Now create ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," file (that may look something like this):"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-json",metastring:"title=./tsconfig.json",title:"./tsconfig.json"},'{\n "compilerOptions": {\n "allowJs": true,\n "target": "ES2022",\n "lib": [\n "ES2022",\n "DOM",\n "DOM.Iterable"\n ],\n "module": "ES2022",\n "moduleResolution": "Node16",\n "strict": true,\n "resolveJsonModule": true,\n "jsx": "react-jsx",\n "baseUrl": ".",\n "outDir": "./build/ts-cache",\n "paths": {\n "app/*": [\n "app/*"\n ],\n }\n },\n "include": ["./app/**/*", "./build/tmp/types/**/*"],\n "exclude": ["./**/__tests__"]\n}\n')),(0,r.yg)("p",null,"When CLI detects existence of the ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," file, it automatically starts ",(0,r.yg)("strong",{parentName:"p"},"type checking")," and ",(0,r.yg)("strong",{parentName:"p"},"compiling")," files with ",(0,r.yg)("inlineCode",{parentName:"p"},"*.ts")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"*.tsx")," extensions."),(0,r.yg)("p",null,"Keep in mind that the code is still compiled using ",(0,r.yg)("a",{parentName:"p",href:"https://swc.rs/"},"swc"),", the same way JS code is. This means that certain settings in ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," only applies to type checking (like ",(0,r.yg)("inlineCode",{parentName:"p"},"target"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"moduleResolution"),", etc.), but compilation uses it's own settings to match the JS code."),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"You will also probably need to install additional ",(0,r.yg)("inlineCode",{parentName:"p"},"@types/*")," type definition libs to ensure proper support, like react types:"),(0,r.yg)(i.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(s.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npm i -D @types/react @types/react-dom\n"))),(0,r.yg)(s.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add --dev @types/react @types/react-dom\n"))),(0,r.yg)(s.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add -D @types/react @types/react-dom\n"))))),(0,r.yg)("h3",{id:"ima-envdts"},"ima-env.d.ts"),(0,r.yg)("p",null,"Additionally we recommend creating a new ",(0,r.yg)("inlineCode",{parentName:"p"},"ima-env.d.ts")," file in root of your ",(0,r.yg)("inlineCode",{parentName:"p"},"./app")," folder with following contents:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/ima-env.d.ts",title:"./app/ima-env.d.ts"},'/// \n')),(0,r.yg)("p",null,"This adds proper types support for webpack specific imports like images and other files."),(0,r.yg)("h2",{id:"create-ima-app-support"},(0,r.yg)("inlineCode",{parentName:"h2"},"create-ima-app")," support"),(0,r.yg)("p",null,"You can also easily create a typescript base IMA.js application using ",(0,r.yg)("inlineCode",{parentName:"p"},"--typescript")," cli argument when running ",(0,r.yg)("inlineCode",{parentName:"p"},"create-ima-app")," command:"),(0,r.yg)(i.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(s.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx create-ima-app ~/Desktop/ima-ts --typescript\n"))),(0,r.yg)(s.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx create-ima-app ~/Desktop/ima-ts --typescript\n"))),(0,r.yg)(s.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx create-ima-app ~/Desktop/ima-ts --typescript\n")))),(0,r.yg)("h2",{id:"controller-generic-types"},"Controller generic types"),(0,r.yg)("p",null,"The ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractController")," class follows similar principles used in React ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractComponent")," type. There are 3 generic types you can define on the class definition itself."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=AbstractController.ts",title:"AbstractController.ts"},"export class AbstractController<\n S extends PageState = {},\n R extends RouteParams = {},\n SS extends S = S\n> extends Controller;\n")),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("inlineCode",{parentName:"li"},"S")," - Use to define shape of your controller managed state."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("inlineCode",{parentName:"li"},"R")," - Use to define controller's route route params that are extracted to ",(0,r.yg)("inlineCode",{parentName:"li"},"this.params"),"."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("inlineCode",{parentName:"li"},"SS")," - Defaults to ",(0,r.yg)("inlineCode",{parentName:"li"},"S"),", however when you are using any extensions in your controller, that have their own state, you can merge those state types in this generic value, to have proper type support for ",(0,r.yg)("inlineCode",{parentName:"li"},"this.getState()")," method (this will now include all state keys, including ones used in extensions).")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=HomeController.ts",title:"HomeController.ts"},"import { TestExtension, GalleryExtensionState } from './GalleryExtension';\n\nexport type HomeControllerState = {\n cards: Promise;\n message: string;\n name: string;\n};\n\nexport class HomeController extends AbstractController<\n HomeControllerState,\n { detailId?: string },\n HomeControllerState & GalleryExtensionState\n>{\n static $extensions?: Dependencies> = [GalleryExtension];\n\n load(): HomeControllerState {\n const cardsPromise = this.#httpAgent\n .get('http://localhost:3001/static/static/public/cards.json')\n .then(response => response.body);\n\n // `state` contains all merged types from `SS` generic value.\n const state = this.getState();\n\n return {\n message: 'test',\n cards: cardsPromise,\n name: 'nam',\n };\n }\n}\n")),(0,r.yg)("h2",{id:"extending-existing-interfaces"},"Extending existing interfaces"),(0,r.yg)("p",null,"Since you can extend certain features like ",(0,r.yg)("inlineCode",{parentName:"p"},"ComponentUtils")," or settings from within your application or through plugins, and in order to provide type checking for these, we are using specific interfaces that you can extend using ",(0,r.yg)("a",{parentName:"p",href:"https://www.typescriptlang.org/docs/handbook/declaration-merging.html"},"Declaration Merging")," feature."),(0,r.yg)("p",null,"This ensures (when used correctly), that you always have correct static types when using these interfaces, even when they are extended in multiple places."),(0,r.yg)("h3",{id:"extending-utils"},"Extending ",(0,r.yg)("inlineCode",{parentName:"h3"},"Utils")),(0,r.yg)("p",null,"When using component utils, in addition to registering your classes using ",(0,r.yg)("inlineCode",{parentName:"p"},"ComponentUtils")," helper, make sure to also extend ",(0,r.yg)("inlineCode",{parentName:"p"},"Utils")," interface. This adds autocomplete and typechecking to ",(0,r.yg)("inlineCode",{parentName:"p"},"this.utils()")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"useComponentUtils")," in your components."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/config/bind.ts",title:"./app/config/bind.ts"},"declare module '@ima/core' {\n interface Utils {\n $CssClasses: typeof defaultCssClasses;\n }\n}\n\nexport const initBindApp: InitBindFunction = (ns, oc) => {\n oc.get(ComponentUtils).register({\n $CssClasses: '$CssClasses',\n });\n};\n")),(0,r.yg)("h3",{id:"extending-objectcontainer"},"Extending ",(0,r.yg)("inlineCode",{parentName:"h3"},"ObjectContainer")),(0,r.yg)("p",null,"Same goes for defining string aliases in Object container. This adds proper type checking to dependencies definition and ",(0,r.yg)("inlineCode",{parentName:"p"},"oc.get")," autocomplete."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/config/bind.ts",title:"./app/config/bind.ts"},"declare module '@ima/core' {\n interface OCAliasMap {\n $CssClasses: () => typeof cssClassNameProcessor;\n $PageRendererFactory: PageRendererFactory;\n API_KEY: string;\n }\n}\n\nexport const initBindApp: InitBindFunction = (ns, oc) => {\n oc.bind('$CssClasses', function () { return cssClassNameProcessor; });\n oc.bind('$PageRendererFactory', PageRendererFactory);\n oc.constant('API_KEY', '14fasdf');\n};\n")),(0,r.yg)("h3",{id:"extending-settings"},"Extending ",(0,r.yg)("inlineCode",{parentName:"h3"},"Settings")),(0,r.yg)("p",null,"This makes sure you don't have any missing or additional fields in your app settings. Other environments than ",(0,r.yg)("inlineCode",{parentName:"p"},"prod")," have all fields made optional, since they are deeply merged with the ",(0,r.yg)("inlineCode",{parentName:"p"},"prod")," settings."),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"Use ",(0,r.yg)("inlineCode",{parentName:"p"},"?:")," for settings with default values. This applies mostly to plugins.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-ts",metastring:"title=./app/config/settings.ts",title:"./app/config/settings.ts"},"declare module '@ima/core' {\n interface Settings {\n links: Record<'documentation' | 'tutorial' | 'plugins' | 'api', string>;\n }\n}\n\nexport const initSettings: InitSettingsFunction = (ns, oc, config) => {\n return {\n prod: {\n links: {\n documentation: 'https://imajs.io/docs',\n api: 'https://imajs.io/api',\n },\n }\n }\n}\n\n")),(0,r.yg)("h2",{id:"dictionary-localization-keys"},"Dictionary localization keys"),(0,r.yg)("p",null,"When compiling app language files, we also generate dictionary keys during runtime. These are then stored in ",(0,r.yg)("inlineCode",{parentName:"p"},"'./build/tmp/types/dictionary.ts'")," file. Don't forget to include this file in ",(0,r.yg)("inlineCode",{parentName:"p"},"tsconfig.json")," source files array, to have correct static type checking:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-json",metastring:"title=./tsconfig.json",title:"./tsconfig.json"},'{\n "include": ["./app/**/*", "./build/tmp/types/**/*"],\n}\n')),(0,r.yg)("admonition",{type:"note"},(0,r.yg)("p",{parentName:"admonition"},"When used in IMA.js plugins, you can manually extend the ",(0,r.yg)("inlineCode",{parentName:"p"},"DictionaryMap")," interface:"),(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-ts"},"declare module '@ima/core' {\n interface DictionaryMap {\n 'home.intro': string;\n }\n}\n\nexport {};\n"))))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1352a5d9.4a519bbd.js b/assets/js/1352a5d9.c1668a17.js similarity index 98% rename from assets/js/1352a5d9.4a519bbd.js rename to assets/js/1352a5d9.c1668a17.js index 4d1f5ad0a..d2525ed17 100644 --- a/assets/js/1352a5d9.4a519bbd.js +++ b/assets/js/1352a5d9.c1668a17.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[6373],{5680:(e,t,r)=>{r.d(t,{xA:()=>l,yg:()=>y});var n=r(6540);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function o(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),p=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):o(o({},t),e)),r},l=function(e){var t=p(e.components);return n.createElement(s.Provider,{value:t},e.children)},d="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,i=e.originalType,s=e.parentName,l=c(e,["components","mdxType","originalType","parentName"]),d=p(r),m=a,y=d["".concat(s,".").concat(m)]||d[m]||u[m]||i;return r?n.createElement(y,o(o({ref:t},l),{},{components:r})):n.createElement(y,o({ref:t},l))}));function y(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=r.length,o=new Array(i);o[0]=m;var c={};for(var s in t)hasOwnProperty.call(t,s)&&(c[s]=t[s]);c.originalType=e,c[d]="string"==typeof e?e:a,o[1]=c;for(var p=2;p{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>u,frontMatter:()=>i,metadata:()=>c,toc:()=>p});var n=r(8102),a=(r(6540),r(5680));const i={title:"Dynamic imports",description:"Advanced Features > Dynamic imports and lazy loading"},o=void 0,c={unversionedId:"advanced-features/dynamic-imports",id:"advanced-features/dynamic-imports",title:"Dynamic imports",description:"Advanced Features > Dynamic imports and lazy loading",source:"@site/../docs/advanced-features/dynamic-imports.md",sourceDirName:"advanced-features",slug:"/advanced-features/dynamic-imports",permalink:"/advanced-features/dynamic-imports",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/advanced-features/dynamic-imports.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Dynamic imports",description:"Advanced Features > Dynamic imports and lazy loading"},sidebar:"docs",previous:{title:"Testing",permalink:"/basic-features/testing"},next:{title:"Introduction to @ima/cli",permalink:"/cli/"}},s={},p=[{value:"Dynamic imports",id:"dynamic-imports",level:2},{value:"Preloading and prefetching",id:"preloading-and-prefetching",level:2},{value:"React suspense",id:"react-suspense",level:2}],l={toc:p},d="wrapper";function u(e){let{components:t,...r}=e;return(0,a.yg)(d,(0,n.A)({},l,r,{components:t,mdxType:"MDXLayout"}),(0,a.yg)("h2",{id:"dynamic-imports"},"Dynamic imports"),(0,a.yg)("h2",{id:"preloading-and-prefetching"},"Preloading and prefetching"),(0,a.yg)("p",null,"Since we're using webpack, to built the application, it already has support for ",(0,a.yg)("a",{parentName:"p",href:"https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules"},"inline directives")," for preloading and prefetching. Using this comment:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-js"},"import(/* webpackPrefetch: true */ './path/to/LoginModal.js');\n")),(0,a.yg)("p",null,"will result in",(0,a.yg)("inlineCode",{parentName:"p"},'')," being appended in the head of the page. For more information about"),(0,a.yg)("h2",{id:"react-suspense"},"React suspense"),(0,a.yg)("p",null,(0,a.yg)("a",{parentName:"p",href:"https://reactjs.org/docs/react-api.html#reactsuspense"},"Suspense")," currently ",(0,a.yg)("strong",{parentName:"p"},"doesn't support SSR"),". However you can use it to load client-side react components. Don't forget to add proper handlers so it only gets rendered on client, since SSR will result in an hydratation error."))}u.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[6373],{5680:(e,t,r)=>{r.d(t,{xA:()=>l,yg:()=>y});var n=r(6540);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function o(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),p=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):o(o({},t),e)),r},l=function(e){var t=p(e.components);return n.createElement(s.Provider,{value:t},e.children)},d="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,i=e.originalType,s=e.parentName,l=c(e,["components","mdxType","originalType","parentName"]),d=p(r),m=a,y=d["".concat(s,".").concat(m)]||d[m]||u[m]||i;return r?n.createElement(y,o(o({ref:t},l),{},{components:r})):n.createElement(y,o({ref:t},l))}));function y(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=r.length,o=new Array(i);o[0]=m;var c={};for(var s in t)hasOwnProperty.call(t,s)&&(c[s]=t[s]);c.originalType=e,c[d]="string"==typeof e?e:a,o[1]=c;for(var p=2;p{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>u,frontMatter:()=>i,metadata:()=>c,toc:()=>p});var n=r(8102),a=(r(6540),r(5680));const i={title:"Dynamic imports",description:"Advanced Features > Dynamic imports and lazy loading"},o=void 0,c={unversionedId:"advanced-features/dynamic-imports",id:"advanced-features/dynamic-imports",title:"Dynamic imports",description:"Advanced Features > Dynamic imports and lazy loading",source:"@site/../docs/advanced-features/dynamic-imports.md",sourceDirName:"advanced-features",slug:"/advanced-features/dynamic-imports",permalink:"/advanced-features/dynamic-imports",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/advanced-features/dynamic-imports.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Dynamic imports",description:"Advanced Features > Dynamic imports and lazy loading"},sidebar:"docs",previous:{title:"Testing",permalink:"/basic-features/testing"},next:{title:"Introduction to @ima/cli",permalink:"/cli/"}},s={},p=[{value:"Dynamic imports",id:"dynamic-imports",level:2},{value:"Preloading and prefetching",id:"preloading-and-prefetching",level:2},{value:"React suspense",id:"react-suspense",level:2}],l={toc:p},d="wrapper";function u(e){let{components:t,...r}=e;return(0,a.yg)(d,(0,n.A)({},l,r,{components:t,mdxType:"MDXLayout"}),(0,a.yg)("h2",{id:"dynamic-imports"},"Dynamic imports"),(0,a.yg)("h2",{id:"preloading-and-prefetching"},"Preloading and prefetching"),(0,a.yg)("p",null,"Since we're using webpack, to built the application, it already has support for ",(0,a.yg)("a",{parentName:"p",href:"https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules"},"inline directives")," for preloading and prefetching. Using this comment:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-js"},"import(/* webpackPrefetch: true */ './path/to/LoginModal.js');\n")),(0,a.yg)("p",null,"will result in",(0,a.yg)("inlineCode",{parentName:"p"},'')," being appended in the head of the page. For more information about"),(0,a.yg)("h2",{id:"react-suspense"},"React suspense"),(0,a.yg)("p",null,(0,a.yg)("a",{parentName:"p",href:"https://reactjs.org/docs/react-api.html#reactsuspense"},"Suspense")," currently ",(0,a.yg)("strong",{parentName:"p"},"doesn't support SSR"),". However you can use it to load client-side react components. Don't forget to add proper handlers so it only gets rendered on client, since SSR will result in an hydratation error."))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/162a65f7.556cde95.js b/assets/js/162a65f7.840432ea.js similarity index 99% rename from assets/js/162a65f7.556cde95.js rename to assets/js/162a65f7.840432ea.js index a89750c51..4c67e6c16 100644 --- a/assets/js/162a65f7.556cde95.js +++ b/assets/js/162a65f7.840432ea.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[3403],{7346:(e,n,a)=>{a.d(n,{A:()=>t});const t=a.p+"assets/images/diagram-page-manager-e1a61acbae5ac5651fe727dd2c2a8c31.png"},5680:(e,n,a)=>{a.d(n,{xA:()=>g,yg:()=>h});var t=a(6540);function r(e,n,a){return n in e?Object.defineProperty(e,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[n]=a,e}function o(e,n){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);n&&(t=t.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),a.push.apply(a,t)}return a}function i(e){for(var n=1;n=0||(r[a]=e[a]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var s=t.createContext({}),d=function(e){var n=t.useContext(s),a=n;return e&&(a="function"==typeof e?e(n):i(i({},n),e)),a},g=function(e){var n=d(e.components);return t.createElement(s.Provider,{value:n},e.children)},p="mdxType",c={inlineCode:"code",wrapper:function(e){var n=e.children;return t.createElement(t.Fragment,{},n)}},m=t.forwardRef((function(e,n){var a=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,g=l(e,["components","mdxType","originalType","parentName"]),p=d(a),m=r,h=p["".concat(s,".").concat(m)]||p[m]||c[m]||o;return a?t.createElement(h,i(i({ref:n},g),{},{components:a})):t.createElement(h,i({ref:n},g))}));function h(e,n){var a=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=a.length,i=new Array(o);i[0]=m;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[p]="string"==typeof e?e:r,i[1]=l;for(var d=2;d{a.r(n),a.d(n,{assets:()=>s,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>l,toc:()=>d});var t=a(8102),r=(a(6540),a(5680));const o={title:"Page Manager",description:"Basic features > Page Manager and app rendering"},i=void 0,l={unversionedId:"basic-features/page-manager",id:"basic-features/page-manager",title:"Page Manager",description:"Basic features > Page Manager and app rendering",source:"@site/../docs/basic-features/page-manager.md",sourceDirName:"basic-features",slug:"/basic-features/page-manager",permalink:"/basic-features/page-manager",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/page-manager.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Page Manager",description:"Basic features > Page Manager and app rendering"},sidebar:"docs",previous:{title:"Handling scripts and styles",permalink:"/basic-features/handling-scripts-and-styles"},next:{title:"Events",permalink:"/basic-features/events"}},s={},d=[{value:"Managing process",id:"managing-process",level:2},{value:"Intervene into the process",id:"intervene-into-the-process",level:2},{value:"PageManagerHandlers",id:"pagemanagerhandlers",level:3},{value:"1. init() method",id:"1-init-method",level:4},{value:"2. handlePreManagedState() method",id:"2-handlepremanagedstate-method",level:4},{value:"3. handlePostManagedState() method",id:"3-handlepostmanagedstate-method",level:4},{value:"4. destroy() method",id:"4-destroy-method",level:4},{value:"Registering PageManagerHandlers",id:"registering-pagemanagerhandlers",level:2},{value:"PageNavigationHandler",id:"pagenavigationhandler",level:2}],g={toc:d},p="wrapper";function c(e){let{components:n,...o}=e;return(0,r.yg)(p,(0,t.A)({},g,o,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Page Manager is an essential part of IMA.js. It's something like a puppeteer that manipulates with pages and views. Once a router matches URL to one of route's path the page manager takes care of the rest."),(0,r.yg)("p",null,(0,r.yg)("img",{src:a(7346).A,width:"881",height:"421"})),(0,r.yg)("h2",{id:"managing-process"},"Managing process"),(0,r.yg)("p",null,"If the new matched route has ",(0,r.yg)("a",{parentName:"p",href:"./routing/introduction#onlyupdate"},(0,r.yg)("inlineCode",{parentName:"a"},"onlyUpdate")," option")," set to ",(0,r.yg)("inlineCode",{parentName:"p"},"true")," and the controller and view hasn't changed the route transition is dispatched only through ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#update-client"},(0,r.yg)("inlineCode",{parentName:"a"},"update")," method")," of the controller."),(0,r.yg)("p",null,"In every other case the manager goes through it's full process:"),(0,r.yg)("ol",null,(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Unload previous controller and extensions")," - To make room for the new, manager has to get rid of the old controller and extensions. First calls ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#deactivate-client"},(0,r.yg)("inlineCode",{parentName:"a"},"deactivate")," method")," on every extension registered in the old controller and then the same method on the controller itself.\nSame process follows with ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#destroy-client"},(0,r.yg)("inlineCode",{parentName:"a"},"destroy")," method"),".")),(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Clear state and unmount view")," - After unloading controller and extensions the page state is cleared and view (starting from ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#managedrootview"},"ManagedRootView"),") is unmounted. However if the ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#documentview"},"DocumentView"),", ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#viewadapter"},"ViewAdapter")," and ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#managedrootview"},"ManagedRootView")," are the same for the new route the view is cleared rather then unmounted. This way you can achieve component persistency.")),(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Loading new controller and extensions")," - After the manager is done with clearing previous resource it initializes the new ones. First the ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#init-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"init")," method")," is called on controller then on every extension (Extensions may ",(0,r.yg)("a",{parentName:"p",href:"./extensions#how-to-use-extensions"},"be initialized")," during the controllers ",(0,r.yg)("inlineCode",{parentName:"p"},"init")," method call).\nWhen the initialization is complete manager starts loading resources via ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method of the controller and extensions. For detailed explanation see the ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#load-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"load")," method documentation"),".")),(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Rendering new view")," - After the ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method has been called a view for the controller is rendered. It doesn't matter if all promises returned by the ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method have been resolved. The process of handling promises is described in the ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#load-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"load")," method documentation"),". Following rendering process is described on a page ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process"},"Rendering process")," and ",(0,r.yg)("a",{parentName:"p",href:"./views-and-components"},"View & Components"),"."))),(0,r.yg)("h2",{id:"intervene-into-the-process"},"Intervene into the process"),(0,r.yg)("p",null,"It's possible for you to intervene into the process before it starts and after it finished. One way is to listen to ",(0,r.yg)("a",{parentName:"p",href:"./events#built-in-events"},(0,r.yg)("inlineCode",{parentName:"a"},"BEFORE_HANDLE_ROUTE"))," and ",(0,r.yg)("a",{parentName:"p",href:"./events#built-in-events"},(0,r.yg)("inlineCode",{parentName:"a"},"AFTER_HANDLE_ROUTE"))," dispatcher events. However from inside event listeners you cannot intercept or modify the process. For this purpose we've introduced PageManagerHandlers in ",(0,r.yg)("a",{parentName:"p",href:"/migration/migration-0.16.0"},"v16")),(0,r.yg)("h3",{id:"pagemanagerhandlers"},"PageManagerHandlers"),(0,r.yg)("p",null,"PageManagerHandler is a simple class that extends ",(0,r.yg)("inlineCode",{parentName:"p"},"ima/page/handler/PageHandler"),". It can obtain dependencies through ",(0,r.yg)("a",{parentName:"p",href:"./object-container#1-dependency-injection"},"dependency injection"),". Each handler should contain 4 methods:"),(0,r.yg)("h4",{id:"1-init-method"},"1. ",(0,r.yg)("inlineCode",{parentName:"h4"},"init()")," method"),(0,r.yg)("p",null,"For purpose of initializing."),(0,r.yg)("h4",{id:"2-handlepremanagedstate-method"},"2. ",(0,r.yg)("inlineCode",{parentName:"h4"},"handlePreManagedState()")," method"),(0,r.yg)("p",null,"This method is called before the page manager start taking any action. It receives 3 arguments ",(0,r.yg)("inlineCode",{parentName:"p"},"managedPage"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"nextManagedPage")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"action"),". ",(0,r.yg)("inlineCode",{parentName:"p"},"managedPage")," holds information about current page, ",(0,r.yg)("inlineCode",{parentName:"p"},"nextManagedPage"),' about following page. Each of the "managed page" arguments has following shape:'),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"{\n controller: ?(string|function(new: Controller)), // controller class\n controllerInstance: ?Controller, // instantiated controller\n decoratedController: ?Controller, // controller decorator created from controller instance\n view: ?React.Component, // view class/component\n viewInstance: ?React.Element, // instantiated view\n route: ?Route, // matched route that leads to the controller\n options: ?RouteOptions, // route options\n params: ?Object, // route parameters and their values\n state: {\n activated: boolean // if the page has been activated\n }\n}\n")),(0,r.yg)("p",null,"and finally the ",(0,r.yg)("inlineCode",{parentName:"p"},"action")," is an object describing what triggered the routing. If a ",(0,r.yg)("inlineCode",{parentName:"p"},"PopStateEvent")," triggered the routing the action object will look like this: ",(0,r.yg)("inlineCode",{parentName:"p"},"{ type: 'popstate', event: PopStateEvent }")," otherwise the ",(0,r.yg)("inlineCode",{parentName:"p"},"event")," property will contain ",(0,r.yg)("inlineCode",{parentName:"p"},"MouseEvent")," (e.g. clicked on a link) and ",(0,r.yg)("inlineCode",{parentName:"p"},"type")," property will have value ",(0,r.yg)("inlineCode",{parentName:"p"},"'redirect'"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"'click'")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"'error'"),"."),(0,r.yg)("h4",{id:"3-handlepostmanagedstate-method"},"3. ",(0,r.yg)("inlineCode",{parentName:"h4"},"handlePostManagedState()")," method"),(0,r.yg)("p",null,"This method is a counterpart to ",(0,r.yg)("inlineCode",{parentName:"p"},"handlePreManagedState()")," method. It's called after page transition is finished. It receives similar arguments (",(0,r.yg)("inlineCode",{parentName:"p"},"managedPage"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"previousManagedPage")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"action"),"). ",(0,r.yg)("inlineCode",{parentName:"p"},"previousManagedPage")," holds information about previous page."),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note:")," ",(0,r.yg)("inlineCode",{parentName:"p"},"handlePreManagedState()")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"handlePostManagedState()")," methods can interrupt transition process by throwing an error. The thrown error should be instance of ",(0,r.yg)("a",{parentName:"p",href:"./error-handling"},(0,r.yg)("inlineCode",{parentName:"a"},"GenericError"))," with a status code specified. That way the router can handle thrown error accordingly.")),(0,r.yg)("h4",{id:"4-destroy-method"},"4. ",(0,r.yg)("inlineCode",{parentName:"h4"},"destroy()")," method"),(0,r.yg)("p",null,"For purpose of destructing"),(0,r.yg)("h2",{id:"registering-pagemanagerhandlers"},"Registering PageManagerHandlers"),(0,r.yg)("p",null,"PageManagerHandlers have their own registry ",(0,r.yg)("strong",{parentName:"p"},"PageHandlerRegistry"),". Every handler you create should be registered as a dependency of this registry."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\nimport { PageHandlerRegistry, Window } from '@ima/core';\nimport MyOwnHandler from 'app/handler/MyOwnHandler';\n\nexport let init = (ns, oc, config) => {\n // ...\n\n if (oc.get(Window).isClient()) { // register different handlers for client and server\n oc.inject(PageHandlerRegistry, [MyOwnHandler]);\n } else {\n oc.inject(PageHandlerRegistry, []);\n }\n};\n")),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note:"),"\xa0Handlers are executed in series and each one waits for the previous one to complete its task.")),(0,r.yg)("h2",{id:"pagenavigationhandler"},"PageNavigationHandler"),(0,r.yg)("p",null,"With introduction of PageManagerHandlers in ",(0,r.yg)("a",{parentName:"p",href:"/migration/migration-0.16.0"},"v16")," we've moved some functionality to predefined handler ",(0,r.yg)("a",{parentName:"p",href:"https://github.com/seznam/ima/blob/master/packages/core/src/page/handler/PageNavigationHandler.js"},(0,r.yg)("strong",{parentName:"a"},"PageNavigationHandler")),". This handler takes care of saving scroll position, restoring scroll position and settings browser's address bar URL. You're free to extend it, override it or whatever else you want."),(0,r.yg)("p",null,"PageNavigationHandler is registered by default, but when you register your own handlers you need to specify PageNavigationHandler as well."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"import { PageHandlerRegistry, PageNavigationHandler } from '@ima/core';\nimport MyOwnHandler from 'app/handler/MyOwnHandler';\n\nexport let init = (ns, oc, config) => {\n // ...\n oc.inject(PageHandlerRegistry, [PageNavigationHandler, MyOwnHandler]);\n};\n")))}c.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[3403],{7346:(e,n,a)=>{a.d(n,{A:()=>t});const t=a.p+"assets/images/diagram-page-manager-e1a61acbae5ac5651fe727dd2c2a8c31.png"},5680:(e,n,a)=>{a.d(n,{xA:()=>g,yg:()=>h});var t=a(6540);function r(e,n,a){return n in e?Object.defineProperty(e,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[n]=a,e}function o(e,n){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);n&&(t=t.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),a.push.apply(a,t)}return a}function i(e){for(var n=1;n=0||(r[a]=e[a]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var s=t.createContext({}),d=function(e){var n=t.useContext(s),a=n;return e&&(a="function"==typeof e?e(n):i(i({},n),e)),a},g=function(e){var n=d(e.components);return t.createElement(s.Provider,{value:n},e.children)},p="mdxType",c={inlineCode:"code",wrapper:function(e){var n=e.children;return t.createElement(t.Fragment,{},n)}},m=t.forwardRef((function(e,n){var a=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,g=l(e,["components","mdxType","originalType","parentName"]),p=d(a),m=r,h=p["".concat(s,".").concat(m)]||p[m]||c[m]||o;return a?t.createElement(h,i(i({ref:n},g),{},{components:a})):t.createElement(h,i({ref:n},g))}));function h(e,n){var a=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=a.length,i=new Array(o);i[0]=m;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[p]="string"==typeof e?e:r,i[1]=l;for(var d=2;d{a.r(n),a.d(n,{assets:()=>s,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>l,toc:()=>d});var t=a(8102),r=(a(6540),a(5680));const o={title:"Page Manager",description:"Basic features > Page Manager and app rendering"},i=void 0,l={unversionedId:"basic-features/page-manager",id:"basic-features/page-manager",title:"Page Manager",description:"Basic features > Page Manager and app rendering",source:"@site/../docs/basic-features/page-manager.md",sourceDirName:"basic-features",slug:"/basic-features/page-manager",permalink:"/basic-features/page-manager",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/page-manager.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Page Manager",description:"Basic features > Page Manager and app rendering"},sidebar:"docs",previous:{title:"Handling scripts and styles",permalink:"/basic-features/handling-scripts-and-styles"},next:{title:"Events",permalink:"/basic-features/events"}},s={},d=[{value:"Managing process",id:"managing-process",level:2},{value:"Intervene into the process",id:"intervene-into-the-process",level:2},{value:"PageManagerHandlers",id:"pagemanagerhandlers",level:3},{value:"1. init() method",id:"1-init-method",level:4},{value:"2. handlePreManagedState() method",id:"2-handlepremanagedstate-method",level:4},{value:"3. handlePostManagedState() method",id:"3-handlepostmanagedstate-method",level:4},{value:"4. destroy() method",id:"4-destroy-method",level:4},{value:"Registering PageManagerHandlers",id:"registering-pagemanagerhandlers",level:2},{value:"PageNavigationHandler",id:"pagenavigationhandler",level:2}],g={toc:d},p="wrapper";function c(e){let{components:n,...o}=e;return(0,r.yg)(p,(0,t.A)({},g,o,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Page Manager is an essential part of IMA.js. It's something like a puppeteer that manipulates with pages and views. Once a router matches URL to one of route's path the page manager takes care of the rest."),(0,r.yg)("p",null,(0,r.yg)("img",{src:a(7346).A,width:"881",height:"421"})),(0,r.yg)("h2",{id:"managing-process"},"Managing process"),(0,r.yg)("p",null,"If the new matched route has ",(0,r.yg)("a",{parentName:"p",href:"./routing/introduction#onlyupdate"},(0,r.yg)("inlineCode",{parentName:"a"},"onlyUpdate")," option")," set to ",(0,r.yg)("inlineCode",{parentName:"p"},"true")," and the controller and view hasn't changed the route transition is dispatched only through ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#update-client"},(0,r.yg)("inlineCode",{parentName:"a"},"update")," method")," of the controller."),(0,r.yg)("p",null,"In every other case the manager goes through it's full process:"),(0,r.yg)("ol",null,(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Unload previous controller and extensions")," - To make room for the new, manager has to get rid of the old controller and extensions. First calls ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#deactivate-client"},(0,r.yg)("inlineCode",{parentName:"a"},"deactivate")," method")," on every extension registered in the old controller and then the same method on the controller itself.\nSame process follows with ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#destroy-client"},(0,r.yg)("inlineCode",{parentName:"a"},"destroy")," method"),".")),(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Clear state and unmount view")," - After unloading controller and extensions the page state is cleared and view (starting from ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#managedrootview"},"ManagedRootView"),") is unmounted. However if the ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#documentview"},"DocumentView"),", ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#viewadapter"},"ViewAdapter")," and ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process#managedrootview"},"ManagedRootView")," are the same for the new route the view is cleared rather then unmounted. This way you can achieve component persistency.")),(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Loading new controller and extensions")," - After the manager is done with clearing previous resource it initializes the new ones. First the ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#init-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"init")," method")," is called on controller then on every extension (Extensions may ",(0,r.yg)("a",{parentName:"p",href:"./extensions#how-to-use-extensions"},"be initialized")," during the controllers ",(0,r.yg)("inlineCode",{parentName:"p"},"init")," method call).\nWhen the initialization is complete manager starts loading resources via ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method of the controller and extensions. For detailed explanation see the ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#load-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"load")," method documentation"),".")),(0,r.yg)("li",{parentName:"ol"},(0,r.yg)("p",{parentName:"li"},(0,r.yg)("strong",{parentName:"p"},"Rendering new view")," - After the ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method has been called a view for the controller is rendered. It doesn't matter if all promises returned by the ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method have been resolved. The process of handling promises is described in the ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#load-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"load")," method documentation"),". Following rendering process is described on a page ",(0,r.yg)("a",{parentName:"p",href:"./rendering-process"},"Rendering process")," and ",(0,r.yg)("a",{parentName:"p",href:"./views-and-components"},"View & Components"),"."))),(0,r.yg)("h2",{id:"intervene-into-the-process"},"Intervene into the process"),(0,r.yg)("p",null,"It's possible for you to intervene into the process before it starts and after it finished. One way is to listen to ",(0,r.yg)("a",{parentName:"p",href:"./events#built-in-events"},(0,r.yg)("inlineCode",{parentName:"a"},"BEFORE_HANDLE_ROUTE"))," and ",(0,r.yg)("a",{parentName:"p",href:"./events#built-in-events"},(0,r.yg)("inlineCode",{parentName:"a"},"AFTER_HANDLE_ROUTE"))," dispatcher events. However from inside event listeners you cannot intercept or modify the process. For this purpose we've introduced PageManagerHandlers in ",(0,r.yg)("a",{parentName:"p",href:"/migration/migration-0.16.0"},"v16")),(0,r.yg)("h3",{id:"pagemanagerhandlers"},"PageManagerHandlers"),(0,r.yg)("p",null,"PageManagerHandler is a simple class that extends ",(0,r.yg)("inlineCode",{parentName:"p"},"ima/page/handler/PageHandler"),". It can obtain dependencies through ",(0,r.yg)("a",{parentName:"p",href:"./object-container#1-dependency-injection"},"dependency injection"),". Each handler should contain 4 methods:"),(0,r.yg)("h4",{id:"1-init-method"},"1. ",(0,r.yg)("inlineCode",{parentName:"h4"},"init()")," method"),(0,r.yg)("p",null,"For purpose of initializing."),(0,r.yg)("h4",{id:"2-handlepremanagedstate-method"},"2. ",(0,r.yg)("inlineCode",{parentName:"h4"},"handlePreManagedState()")," method"),(0,r.yg)("p",null,"This method is called before the page manager start taking any action. It receives 3 arguments ",(0,r.yg)("inlineCode",{parentName:"p"},"managedPage"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"nextManagedPage")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"action"),". ",(0,r.yg)("inlineCode",{parentName:"p"},"managedPage")," holds information about current page, ",(0,r.yg)("inlineCode",{parentName:"p"},"nextManagedPage"),' about following page. Each of the "managed page" arguments has following shape:'),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"{\n controller: ?(string|function(new: Controller)), // controller class\n controllerInstance: ?Controller, // instantiated controller\n decoratedController: ?Controller, // controller decorator created from controller instance\n view: ?React.Component, // view class/component\n viewInstance: ?React.Element, // instantiated view\n route: ?Route, // matched route that leads to the controller\n options: ?RouteOptions, // route options\n params: ?Object, // route parameters and their values\n state: {\n activated: boolean // if the page has been activated\n }\n}\n")),(0,r.yg)("p",null,"and finally the ",(0,r.yg)("inlineCode",{parentName:"p"},"action")," is an object describing what triggered the routing. If a ",(0,r.yg)("inlineCode",{parentName:"p"},"PopStateEvent")," triggered the routing the action object will look like this: ",(0,r.yg)("inlineCode",{parentName:"p"},"{ type: 'popstate', event: PopStateEvent }")," otherwise the ",(0,r.yg)("inlineCode",{parentName:"p"},"event")," property will contain ",(0,r.yg)("inlineCode",{parentName:"p"},"MouseEvent")," (e.g. clicked on a link) and ",(0,r.yg)("inlineCode",{parentName:"p"},"type")," property will have value ",(0,r.yg)("inlineCode",{parentName:"p"},"'redirect'"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"'click'")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"'error'"),"."),(0,r.yg)("h4",{id:"3-handlepostmanagedstate-method"},"3. ",(0,r.yg)("inlineCode",{parentName:"h4"},"handlePostManagedState()")," method"),(0,r.yg)("p",null,"This method is a counterpart to ",(0,r.yg)("inlineCode",{parentName:"p"},"handlePreManagedState()")," method. It's called after page transition is finished. It receives similar arguments (",(0,r.yg)("inlineCode",{parentName:"p"},"managedPage"),", ",(0,r.yg)("inlineCode",{parentName:"p"},"previousManagedPage")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"action"),"). ",(0,r.yg)("inlineCode",{parentName:"p"},"previousManagedPage")," holds information about previous page."),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note:")," ",(0,r.yg)("inlineCode",{parentName:"p"},"handlePreManagedState()")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"handlePostManagedState()")," methods can interrupt transition process by throwing an error. The thrown error should be instance of ",(0,r.yg)("a",{parentName:"p",href:"./error-handling"},(0,r.yg)("inlineCode",{parentName:"a"},"GenericError"))," with a status code specified. That way the router can handle thrown error accordingly.")),(0,r.yg)("h4",{id:"4-destroy-method"},"4. ",(0,r.yg)("inlineCode",{parentName:"h4"},"destroy()")," method"),(0,r.yg)("p",null,"For purpose of destructing"),(0,r.yg)("h2",{id:"registering-pagemanagerhandlers"},"Registering PageManagerHandlers"),(0,r.yg)("p",null,"PageManagerHandlers have their own registry ",(0,r.yg)("strong",{parentName:"p"},"PageHandlerRegistry"),". Every handler you create should be registered as a dependency of this registry."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\nimport { PageHandlerRegistry, Window } from '@ima/core';\nimport MyOwnHandler from 'app/handler/MyOwnHandler';\n\nexport let init = (ns, oc, config) => {\n // ...\n\n if (oc.get(Window).isClient()) { // register different handlers for client and server\n oc.inject(PageHandlerRegistry, [MyOwnHandler]);\n } else {\n oc.inject(PageHandlerRegistry, []);\n }\n};\n")),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note:"),"\xa0Handlers are executed in series and each one waits for the previous one to complete its task.")),(0,r.yg)("h2",{id:"pagenavigationhandler"},"PageNavigationHandler"),(0,r.yg)("p",null,"With introduction of PageManagerHandlers in ",(0,r.yg)("a",{parentName:"p",href:"/migration/migration-0.16.0"},"v16")," we've moved some functionality to predefined handler ",(0,r.yg)("a",{parentName:"p",href:"https://github.com/seznam/ima/blob/master/packages/core/src/page/handler/PageNavigationHandler.js"},(0,r.yg)("strong",{parentName:"a"},"PageNavigationHandler")),". This handler takes care of saving scroll position, restoring scroll position and settings browser's address bar URL. You're free to extend it, override it or whatever else you want."),(0,r.yg)("p",null,"PageNavigationHandler is registered by default, but when you register your own handlers you need to specify PageNavigationHandler as well."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"import { PageHandlerRegistry, PageNavigationHandler } from '@ima/core';\nimport MyOwnHandler from 'app/handler/MyOwnHandler';\n\nexport let init = (ns, oc, config) => {\n // ...\n oc.inject(PageHandlerRegistry, [PageNavigationHandler, MyOwnHandler]);\n};\n")))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1ba2ef78.5068da48.js b/assets/js/1ba2ef78.244e67e1.js similarity index 99% rename from assets/js/1ba2ef78.5068da48.js rename to assets/js/1ba2ef78.244e67e1.js index e3f575ba9..f70f1e222 100644 --- a/assets/js/1ba2ef78.5068da48.js +++ b/assets/js/1ba2ef78.244e67e1.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[4355],{5680:(e,t,n)=>{n.d(t,{xA:()=>c,yg:()=>g});var o=n(6540);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function i(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=o.createContext({}),p=function(e){var t=o.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},c=function(e){var t=p(e.components);return o.createElement(l.Provider,{value:t},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},d=o.forwardRef((function(e,t){var n=e.components,r=e.mdxType,a=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),u=p(n),d=r,g=u["".concat(l,".").concat(d)]||u[d]||m[d]||a;return n?o.createElement(g,i(i({ref:t},c),{},{components:n})):o.createElement(g,i({ref:t},c))}));function g(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var a=n.length,i=new Array(a);i[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[u]="string"==typeof e?e:r,i[1]=s;for(var p=2;p{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>m,frontMatter:()=>a,metadata:()=>s,toc:()=>p});var o=n(8102),r=(n(6540),n(5680));const a={title:"Async Routing",description:"Basic features > Routing > Async Routing"},i=void 0,s={unversionedId:"basic-features/routing/async-routing",id:"basic-features/routing/async-routing",title:"Async Routing",description:"Basic features > Routing > Async Routing",source:"@site/../docs/basic-features/routing/async-routing.md",sourceDirName:"basic-features/routing",slug:"/basic-features/routing/async-routing",permalink:"/basic-features/routing/async-routing",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/routing/async-routing.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Async Routing",description:"Basic features > Routing > Async Routing"},sidebar:"docs",previous:{title:"Middlewares",permalink:"/basic-features/routing/middlewares"},next:{title:"Extensions",permalink:"/basic-features/extensions"}},l={},p=[{value:"Merging view and controller imports into one",id:"merging-view-and-controller-imports-into-one",level:2},{value:"Preloading routeHandlers",id:"preloading-routehandlers",level:2},{value:"Prefetching/Preloading modules",id:"prefetchingpreloading-modules",level:3}],c={toc:p},u="wrapper";function m(e){let{components:t,...n}=e;return(0,r.yg)(u,(0,o.A)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Async routing allows you to split views and controllers into separate bundles and load them dynamically. This can be useful for some specific routes, that are not visited regularly and contain large amounts of unique code."),(0,r.yg)("p",null,"To take advantage of this feature, you simply wrap your ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#controller"},(0,r.yg)("inlineCode",{parentName:"a"},"controller"))," and ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#view"},(0,r.yg)("inlineCode",{parentName:"a"},"view"))," arguments into ",(0,r.yg)("inlineCode",{parentName:"p"},"async")," function which calls a dynamic import():"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add(\n 'home',\n '/',\n async() => import('app/page/home/HomeController'),\n async() => import('app/page/home/HomeView')\n )\n}\n")),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"When using ",(0,r.yg)("strong",{parentName:"p"},"default exports"),", you don't have to explicitly set the import promise to the default export, the router does this by default."),(0,r.yg)("p",{parentName:"admonition"},"However when using named exports you need to let the router know, where is the controller/view located in the resolved promise:"),(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"async() => import('app/page/home/HomeView').then(module => module.HomeView);\n"))),(0,r.yg)("h2",{id:"merging-view-and-controller-imports-into-one"},"Merging view and controller imports into one"),(0,r.yg)("p",null,"Since the method above produces 2 separate JS chunk files (can depend on the actual environment). If you have really small controller and view files, you can help webpack in creating only one small chunk file which usually loads faster."),(0,r.yg)("p",null,"This can be done by exporting view and controller from the same file:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/page/home/index.js",title:"./app/page/home/index.js"},"export { default as HomeView } from './HomeView';\nexport { default as HomeController } from './HomeController';\n")),(0,r.yg)("p",null,"And then merging those two dynamic imports into one:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nconst homeModules = async () => import('app/page/home');\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add(\n 'home',\n '/',\n // highlight-next-line\n async () => homeModules().then(module => module.HomeController),\n // highlight-next-line\n async () => homeModules().then(module => module.HomeView)\n )\n}\n")),(0,r.yg)("h2",{id:"preloading-routehandlers"},"Preloading routeHandlers"),(0,r.yg)("p",null,"Each route handler exposes ",(0,r.yg)("inlineCode",{parentName:"p"},"preload()")," method, which can be used to programmatically trigger preload of the dynamic imports for specific route."),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"Use this in situations when the browser is idle and you want to preload some specific route handlers that the user will probably go next. This speeds up the responsiveness of your application dramatically.")),(0,r.yg)("p",null,"To call the ",(0,r.yg)("inlineCode",{parentName:"p"},"preload()")," method, ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#generating-links-outside-of-app-components"},"you first need to get access")," to the ",(0,r.yg)("inlineCode",{parentName:"p"},"Router")," instance (we can use ",(0,r.yg)("inlineCode",{parentName:"p"},"useComponentUtils")," hook in this example) and then you can use ",(0,r.yg)("inlineCode",{parentName:"p"},"getRouteHandler()")," method to get specific route handler instance. After that just call ",(0,r.yg)("inlineCode",{parentName:"p"},"preload()")," on this handler:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-jsx",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { useComponentUtils } from '@ima/react-hooks';\n\nexport default function Card() {\n const { $Router } = useComponentUtils();\n const homeRouteHandler = $Router.getRouteHandler('home');\n\n useEffect(() => {\n // highlight-next-line\n homeRouteHandler.preload();\n }, [])\n\n return (\n Home\n );\n}\n")),(0,r.yg)("p",null,"The method returns a promise, which resolves to tuple of ",(0,r.yg)("inlineCode",{parentName:"p"},"[controller, view]")," instances."),(0,r.yg)("h3",{id:"prefetchingpreloading-modules"},"Prefetching/Preloading modules"),(0,r.yg)("p",null,"As with the ",(0,r.yg)("a",{parentName:"p",href:"/advanced-features/dynamic-imports"},"dynamic imports"),", you can also use ",(0,r.yg)("a",{parentName:"p",href:"https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules"},"webpack directives")," for prefetching and preloading. Simply use the inline commend as it is mentioned in the ",(0,r.yg)("a",{parentName:"p",href:"https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules"},"webpack documentation"),"."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"// ...\nasync() => import(/* webpackPrefetch: true */ 'app/page/home/HomeController'),\nasync() => import(/* webpackPreload: true */ 'app/page/home/HomeView')\n// ...\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[4355],{5680:(e,t,n)=>{n.d(t,{xA:()=>c,yg:()=>g});var o=n(6540);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function i(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=o.createContext({}),p=function(e){var t=o.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},c=function(e){var t=p(e.components);return o.createElement(l.Provider,{value:t},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},d=o.forwardRef((function(e,t){var n=e.components,r=e.mdxType,a=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),u=p(n),d=r,g=u["".concat(l,".").concat(d)]||u[d]||m[d]||a;return n?o.createElement(g,i(i({ref:t},c),{},{components:n})):o.createElement(g,i({ref:t},c))}));function g(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var a=n.length,i=new Array(a);i[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[u]="string"==typeof e?e:r,i[1]=s;for(var p=2;p{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>m,frontMatter:()=>a,metadata:()=>s,toc:()=>p});var o=n(8102),r=(n(6540),n(5680));const a={title:"Async Routing",description:"Basic features > Routing > Async Routing"},i=void 0,s={unversionedId:"basic-features/routing/async-routing",id:"basic-features/routing/async-routing",title:"Async Routing",description:"Basic features > Routing > Async Routing",source:"@site/../docs/basic-features/routing/async-routing.md",sourceDirName:"basic-features/routing",slug:"/basic-features/routing/async-routing",permalink:"/basic-features/routing/async-routing",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/routing/async-routing.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Async Routing",description:"Basic features > Routing > Async Routing"},sidebar:"docs",previous:{title:"Middlewares",permalink:"/basic-features/routing/middlewares"},next:{title:"Extensions",permalink:"/basic-features/extensions"}},l={},p=[{value:"Merging view and controller imports into one",id:"merging-view-and-controller-imports-into-one",level:2},{value:"Preloading routeHandlers",id:"preloading-routehandlers",level:2},{value:"Prefetching/Preloading modules",id:"prefetchingpreloading-modules",level:3}],c={toc:p},u="wrapper";function m(e){let{components:t,...n}=e;return(0,r.yg)(u,(0,o.A)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Async routing allows you to split views and controllers into separate bundles and load them dynamically. This can be useful for some specific routes, that are not visited regularly and contain large amounts of unique code."),(0,r.yg)("p",null,"To take advantage of this feature, you simply wrap your ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#controller"},(0,r.yg)("inlineCode",{parentName:"a"},"controller"))," and ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#view"},(0,r.yg)("inlineCode",{parentName:"a"},"view"))," arguments into ",(0,r.yg)("inlineCode",{parentName:"p"},"async")," function which calls a dynamic import():"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add(\n 'home',\n '/',\n async() => import('app/page/home/HomeController'),\n async() => import('app/page/home/HomeView')\n )\n}\n")),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"When using ",(0,r.yg)("strong",{parentName:"p"},"default exports"),", you don't have to explicitly set the import promise to the default export, the router does this by default."),(0,r.yg)("p",{parentName:"admonition"},"However when using named exports you need to let the router know, where is the controller/view located in the resolved promise:"),(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"async() => import('app/page/home/HomeView').then(module => module.HomeView);\n"))),(0,r.yg)("h2",{id:"merging-view-and-controller-imports-into-one"},"Merging view and controller imports into one"),(0,r.yg)("p",null,"Since the method above produces 2 separate JS chunk files (can depend on the actual environment). If you have really small controller and view files, you can help webpack in creating only one small chunk file which usually loads faster."),(0,r.yg)("p",null,"This can be done by exporting view and controller from the same file:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/page/home/index.js",title:"./app/page/home/index.js"},"export { default as HomeView } from './HomeView';\nexport { default as HomeController } from './HomeController';\n")),(0,r.yg)("p",null,"And then merging those two dynamic imports into one:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nconst homeModules = async () => import('app/page/home');\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add(\n 'home',\n '/',\n // highlight-next-line\n async () => homeModules().then(module => module.HomeController),\n // highlight-next-line\n async () => homeModules().then(module => module.HomeView)\n )\n}\n")),(0,r.yg)("h2",{id:"preloading-routehandlers"},"Preloading routeHandlers"),(0,r.yg)("p",null,"Each route handler exposes ",(0,r.yg)("inlineCode",{parentName:"p"},"preload()")," method, which can be used to programmatically trigger preload of the dynamic imports for specific route."),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"Use this in situations when the browser is idle and you want to preload some specific route handlers that the user will probably go next. This speeds up the responsiveness of your application dramatically.")),(0,r.yg)("p",null,"To call the ",(0,r.yg)("inlineCode",{parentName:"p"},"preload()")," method, ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#generating-links-outside-of-app-components"},"you first need to get access")," to the ",(0,r.yg)("inlineCode",{parentName:"p"},"Router")," instance (we can use ",(0,r.yg)("inlineCode",{parentName:"p"},"useComponentUtils")," hook in this example) and then you can use ",(0,r.yg)("inlineCode",{parentName:"p"},"getRouteHandler()")," method to get specific route handler instance. After that just call ",(0,r.yg)("inlineCode",{parentName:"p"},"preload()")," on this handler:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-jsx",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { useComponentUtils } from '@ima/react-hooks';\n\nexport default function Card() {\n const { $Router } = useComponentUtils();\n const homeRouteHandler = $Router.getRouteHandler('home');\n\n useEffect(() => {\n // highlight-next-line\n homeRouteHandler.preload();\n }, [])\n\n return (\n Home\n );\n}\n")),(0,r.yg)("p",null,"The method returns a promise, which resolves to tuple of ",(0,r.yg)("inlineCode",{parentName:"p"},"[controller, view]")," instances."),(0,r.yg)("h3",{id:"prefetchingpreloading-modules"},"Prefetching/Preloading modules"),(0,r.yg)("p",null,"As with the ",(0,r.yg)("a",{parentName:"p",href:"/advanced-features/dynamic-imports"},"dynamic imports"),", you can also use ",(0,r.yg)("a",{parentName:"p",href:"https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules"},"webpack directives")," for prefetching and preloading. Simply use the inline commend as it is mentioned in the ",(0,r.yg)("a",{parentName:"p",href:"https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules"},"webpack documentation"),"."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"// ...\nasync() => import(/* webpackPrefetch: true */ 'app/page/home/HomeController'),\nasync() => import(/* webpackPreload: true */ 'app/page/home/HomeView')\n// ...\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/201713f2.cb5cc47d.js b/assets/js/201713f2.824b104d.js similarity index 99% rename from assets/js/201713f2.cb5cc47d.js rename to assets/js/201713f2.824b104d.js index 21f7be266..145621d9e 100644 --- a/assets/js/201713f2.cb5cc47d.js +++ b/assets/js/201713f2.824b104d.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[9541],{5680:(e,r,t)=>{t.d(r,{xA:()=>p,yg:()=>g});var a=t(6540);function n(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}function l(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);r&&(a=a.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var r=1;r=0||(n[t]=e[t]);return n}(e,r);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(n[t]=e[t])}return n}var s=a.createContext({}),u=function(e){var r=a.useContext(s),t=r;return e&&(t="function"==typeof e?e(r):i(i({},r),e)),t},p=function(e){var r=u(e.components);return a.createElement(s.Provider,{value:r},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var r=e.children;return a.createElement(a.Fragment,{},r)}},m=a.forwardRef((function(e,r){var t=e.components,n=e.mdxType,l=e.originalType,s=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),c=u(t),m=n,g=c["".concat(s,".").concat(m)]||c[m]||d[m]||l;return t?a.createElement(g,i(i({ref:r},p),{},{components:t})):a.createElement(g,i({ref:r},p))}));function g(e,r){var t=arguments,n=r&&r.mdxType;if("string"==typeof e||n){var l=t.length,i=new Array(l);i[0]=m;var o={};for(var s in r)hasOwnProperty.call(r,s)&&(o[s]=r[s]);o.originalType=e,o[c]="string"==typeof e?e:n,i[1]=o;for(var u=2;u{t.d(r,{A:()=>i});var a=t(6540),n=t(8017);const l={tabItem:"tabItem_Ymn6"};function i(e){let{children:r,hidden:t,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,n.A)(l.tabItem,i),hidden:t},r)}},1253:(e,r,t)=>{t.d(r,{A:()=>j});var a=t(8102),n=t(6540),l=t(8017),i=t(3104),o=t(9519),s=t(7485),u=t(1682),p=t(9466);function c(e){return function(e){return n.Children.map(e,(e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:r}=e;return!!r&&"object"==typeof r&&"value"in r}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:r,label:t,attributes:a,default:n}}=e;return{value:r,label:t,attributes:a,default:n}}))}function d(e){const{values:r,children:t}=e;return(0,n.useMemo)((()=>{const e=r??c(t);return function(e){const r=(0,u.X)(e,((e,r)=>e.value===r.value));if(r.length>0)throw new Error(`Docusaurus error: Duplicate values "${r.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[r,t])}function m(e){let{value:r,tabValues:t}=e;return t.some((e=>e.value===r))}function g(e){let{queryString:r=!1,groupId:t}=e;const a=(0,o.W6)(),l=function(e){let{queryString:r=!1,groupId:t}=e;if("string"==typeof r)return r;if(!1===r)return null;if(!0===r&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:r,groupId:t});return[(0,s.aZ)(l),(0,n.useCallback)((e=>{if(!l)return;const r=new URLSearchParams(a.location.search);r.set(l,e),a.replace({...a.location,search:r.toString()})}),[l,a])]}function y(e){const{defaultValue:r,queryString:t=!1,groupId:a}=e,l=d(e),[i,o]=(0,n.useState)((()=>function(e){let{defaultValue:r,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(r){if(!m({value:r,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${r}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return r}const a=t.find((e=>e.default))??t[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:r,tabValues:l}))),[s,u]=g({queryString:t,groupId:a}),[c,y]=function(e){let{groupId:r}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(r),[a,l]=(0,p.Dv)(t);return[a,(0,n.useCallback)((e=>{t&&l.set(e)}),[t,l])]}({groupId:a}),v=(()=>{const e=s??c;return m({value:e,tabValues:l})?e:null})();(0,n.useLayoutEffect)((()=>{v&&o(v)}),[v]);return{selectedValue:i,selectValue:(0,n.useCallback)((e=>{if(!m({value:e,tabValues:l}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),y(e)}),[u,y,l]),tabValues:l}}var v=t(2303);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function b(e){let{className:r,block:t,selectedValue:o,selectValue:s,tabValues:u}=e;const p=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const r=e.currentTarget,t=p.indexOf(r),a=u[t].value;a!==o&&(c(r),s(a))},m=e=>{let r=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=p.indexOf(e.currentTarget)+1;r=p[t]??p[0];break}case"ArrowLeft":{const t=p.indexOf(e.currentTarget)-1;r=p[t]??p[p.length-1];break}}r?.focus()};return n.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,l.A)("tabs",{"tabs--block":t},r)},u.map((e=>{let{value:r,label:t,attributes:i}=e;return n.createElement("li",(0,a.A)({role:"tab",tabIndex:o===r?0:-1,"aria-selected":o===r,key:r,ref:e=>p.push(e),onKeyDown:m,onClick:d},i,{className:(0,l.A)("tabs__item",f.tabItem,i?.className,{"tabs__item--active":o===r})}),t??r)})))}function h(e){let{lazy:r,children:t,selectedValue:a}=e;const l=(Array.isArray(t)?t:[t]).filter(Boolean);if(r){const e=l.find((e=>e.props.value===a));return e?(0,n.cloneElement)(e,{className:"margin-top--md"}):null}return n.createElement("div",{className:"margin-top--md"},l.map(((e,r)=>(0,n.cloneElement)(e,{key:r,hidden:e.props.value!==a}))))}function N(e){const r=y(e);return n.createElement("div",{className:(0,l.A)("tabs-container",f.tabList)},n.createElement(b,(0,a.A)({},e,r)),n.createElement(h,(0,a.A)({},e,r)))}function j(e){const r=(0,v.A)();return n.createElement(N,(0,a.A)({key:String(r)},e))}},8809:(e,r,t)=>{t.r(r),t.d(r,{assets:()=>p,contentTitle:()=>s,default:()=>g,frontMatter:()=>o,metadata:()=>u,toc:()=>c});var a=t(8102),n=(t(6540),t(5680)),l=t(1253),i=t(6185);const o={title:"Migration 18.0.0",description:"Migration > Migration to version 18.0.0"},s=void 0,u={unversionedId:"migration/migration-18.0.0",id:"migration/migration-18.0.0",title:"Migration 18.0.0",description:"Migration > Migration to version 18.0.0",source:"@site/../docs/migration/migration-18.0.0.md",sourceDirName:"migration",slug:"/migration/migration-18.0.0",permalink:"/migration/migration-18.0.0",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/migration/migration-18.0.0.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Migration 18.0.0",description:"Migration > Migration to version 18.0.0"},sidebar:"docs",previous:{title:"Migration 17.0.0",permalink:"/migration/migration-17.0.0"},next:{title:"Migration 19.0.0",permalink:"/migration/migration-19.0.0"}},p={},c=[{value:"Update requirements",id:"update-requirements",level:2},{value:"Moved from gulp to webpack",id:"moved-from-gulp-to-webpack",level:2},{value:"Change scripts in package.json",id:"change-scripts-in-packagejson",level:3},{value:"Update settings.js",id:"update-settingsjs",level:3},{value:"Remove gulp specific things",id:"remove-gulp-specific-things",level:3},{value:"Removed build.js, optionally add ima.config.js file to root",id:"removed-buildjs-optionally-add-imaconfigjs-file-to-root",level:3},{value:"Moved from babel to swc",id:"moved-from-babel-to-swc",level:2},{value:"New React-page-renderer",id:"new-react-page-renderer",level:2},{value:"Update EventBus",id:"update-eventbus",level:3},{value:"Update DocumentView",id:"update-documentview",level:2},{value:"Update Server",id:"update-server",level:2},{value:"Split server.js -> server.js and app.js",id:"split-serverjs---serverjs-and-appjs",level:3},{value:"Server changes",id:"server-changes",level:3},{value:"Move environment.js file",id:"move-environmentjs-file",level:3},{value:"Templates",id:"templates",level:3},{value:"Update DocumentView",id:"update-documentview-1",level:3},{value:"Assets => app/public",id:"assets--apppublic",level:2},{value:"Styles",id:"styles",level:2},{value:"Tests",id:"tests",level:2},{value:"Other changes",id:"other-changes",level:2},{value:"Deleted packages",id:"deleted-packages",level:2},{value:"IMA.js Plugins",id:"imajs-plugins",level:2}],d={toc:c},m="wrapper";function g(e){let{components:r,...t}=e;return(0,n.yg)(m,(0,a.A)({},d,t,{components:r,mdxType:"MDXLayout"}),(0,n.yg)("p",null,"IMA.js brings few major breaking changes. For more information read below."),(0,n.yg)("h2",{id:"update-requirements"},"Update requirements"),(0,n.yg)("p",null,"IMA.js v18 requires node >= 18, npm >= 8 and react 18."),(0,n.yg)("h2",{id:"moved-from-gulp-to-webpack"},"Moved from gulp to webpack"),(0,n.yg)("p",null,"You can remove gulp things. There is new @ima/cli plugin for helping with webpack.\nFrom now, you have to import everything you want to be present in your bundle (that's how webpack works)."),(0,n.yg)("h3",{id:"change-scripts-in-packagejson"},"Change scripts in package.json"),(0,n.yg)("p",null,"There is new @ima/cli used in scripts instead of gulp."),(0,n.yg)("p",null,(0,n.yg)("strong",{parentName:"p"},"Example:")),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},'...\n "scripts": {\n "test": "jest",\n "lint": "eslint \'./**/*.{js,jsx,ts,tsx}\'",\n "dev": "ima dev",\n "build": "NODE_ENV=production ima build",\n "start": "NODE_ENV=production node server/server.js"\n},\n...\n')),(0,n.yg)("p",null,"Remove ",(0,n.yg)("inlineCode",{parentName:"p"},'"main": "build/server.js"')," from ",(0,n.yg)("inlineCode",{parentName:"p"},"package.json")," too. (Server is not anymore in build/server.js.)"),(0,n.yg)("h3",{id:"update-settingsjs"},"Update settings.js"),(0,n.yg)("p",null,"Remove scripts and esScripts from $Page.$Render (IMA process this things now by manifest and contentVariables)."),(0,n.yg)("h3",{id:"remove-gulp-specific-things"},"Remove gulp specific things"),(0,n.yg)("p",null,"Dependencies:"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-task-loader"),(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-tasks")),(0,n.yg)("p",null,"Files:"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"gulpfile.js"),(0,n.yg)("li",{parentName:"ul"},"gulpConfig.js")),(0,n.yg)("h3",{id:"removed-buildjs-optionally-add-imaconfigjs-file-to-root"},"Removed build.js, optionally add ima.config.js file to root"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"look at ",(0,n.yg)("a",{parentName:"li",href:"../cli/ima-config-js"},"ima.config.js")," "),(0,n.yg)("li",{parentName:"ul"},"definition of languages moved from ",(0,n.yg)("inlineCode",{parentName:"li"},"build.js")," to ",(0,n.yg)("inlineCode",{parentName:"li"},"ima.config.js")),(0,n.yg)("li",{parentName:"ul"},"definition of less file pathes is not needed - see section Styles below")),(0,n.yg)("h2",{id:"moved-from-babel-to-swc"},"Moved from babel to swc"),(0,n.yg)("p",null,"You can remove @babel dependencies (except for eslint specific). "),(0,n.yg)("p",null,"Add ",(0,n.yg)("inlineCode",{parentName:"p"},"@swc/jest")," devDependency for tests."),(0,n.yg)("h2",{id:"new-react-page-renderer"},"New React-page-renderer"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"React-page-renderer moved to new package @ima/react-page-renderer ")),(0,n.yg)(l.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,n.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-bash"},"npm i @ima/react-page-renderer\n"))),(0,n.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add @ima/react-page-renderer\n"))),(0,n.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add @ima/react-page-renderer\n")))),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},(0,n.yg)("p",{parentName:"li"},"You can use codemod ",(0,n.yg)("inlineCode",{parentName:"p"},"npx @cns/web-plugins-codemods")," -> ima18: react page renderer imports")),(0,n.yg)("li",{parentName:"ul"},(0,n.yg)("p",{parentName:"li"},"Update DocumentView - use AbstractPureComponent from @ima/react-page-renderer instead of AbstractDocumentView"))),(0,n.yg)("h3",{id:"update-eventbus"},"Update EventBus"),(0,n.yg)("p",null,"You have to add target as the second argument for EventBus fire, listen/unlisten."),(0,n.yg)("h2",{id:"update-documentview"},"Update DocumentView"),(0,n.yg)("p",null,"Rewrite your DocumentView similar like in create-ima-app."),(0,n.yg)("h2",{id:"update-server"},"Update Server"),(0,n.yg)("p",null,"You have to add dependency to ",(0,n.yg)("inlineCode",{parentName:"p"},"error-to-json")," on your own. It was removed from @ima/server."),(0,n.yg)("p",null,"Replace"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"let errorToJSON = require('error-to-json');\n")),(0,n.yg)("p",null,"by"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"const errorToJSON = require('error-to-json').default;\n")),(0,n.yg)("h3",{id:"split-serverjs---serverjs-and-appjs"},"Split server.js -> server.js and app.js"),(0,n.yg)("p",null,"This change is optionally, but we use it in our create-ima-app."),(0,n.yg)("h3",{id:"server-changes"},"Server changes"),(0,n.yg)("p",null,"Remove:"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"'use strict';\n\nrequire('@ima/core/polyfill/imaLoader.js');\nrequire('@ima/core/polyfill/imaRunner.js');\n")),(0,n.yg)("p",null,"Replace this part:"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"let imaServer = require('@ima/server');\n\nlet clientApp = imaServer.clientApp;\nlet urlParser = imaServer.urlParser;\nlet environment = imaServer.environment;\nlet logger = imaServer.logger;\nlet cache = imaServer.cache;\n")),(0,n.yg)("p",null,"by"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"const imaServer = require('@ima/server')();\nconst { serverApp, urlParser, environment, logger, cache, memStaticProxy } =\n imaServer;\n\nrequire('@ima/react-page-renderer/hook/server')(imaServer);\n")),(0,n.yg)("p",null,"Replace clientApp.requestHandler by serverApp.requestHandlerMiddleware."),(0,n.yg)("p",null,"Remove staticErrorPage and replace errorHandler function by"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"function renderError(error, req, res, next) {\n serverApp\n .errorHandlerMiddleware(error, req, res, next)\n .then(response => {\n logger.error(response.error);\n })\n .catch(next);\n}\n")),(0,n.yg)("h3",{id:"move-environmentjs-file"},"Move environment.js file"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"File ",(0,n.yg)("inlineCode",{parentName:"li"},"app/environment.js")," was moved to location ",(0,n.yg)("inlineCode",{parentName:"li"},"/server/config/environment.js"),"\nThere was removed ",(0,n.yg)("strong",{parentName:"li"},"test")," env.")),(0,n.yg)("h3",{id:"templates"},"Templates"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"400, 500, spa templates are in ",(0,n.yg)("inlineCode",{parentName:"li"},"server/template")," (look at ",(0,n.yg)("a",{parentName:"li",href:"https://github.com/seznam/ima/tree/master/packages/create-ima-app/template/server/template"},"create-ima-app"),")")),(0,n.yg)("h3",{id:"update-documentview-1"},"Update DocumentView"),(0,n.yg)("p",null,"You can remove getAsyncScripts method and body content replace with:\n(You have to add $Page.$Render.masterElementId property to settings.js)"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-jsx"}," \n {'#{revivalCache}'}\n {'#{revivalSettings}'}\n {'#{runner}'}\n")),(0,n.yg)("p",null,"Instead of app css loading use:"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre"}," {'#{styles}'}\n")),(0,n.yg)("h2",{id:"assets--apppublic"},"Assets => app/public"),(0,n.yg)("p",null,"Everything from folder app/public is moved to build folder into static folder."),(0,n.yg)("h2",{id:"styles"},"Styles"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"Remove files mark as ",(0,n.yg)("inlineCode",{parentName:"li"},"FAKE FILE FOR GULP LESS")),(0,n.yg)("li",{parentName:"ul"},"Move less files from ",(0,n.yg)("inlineCode",{parentName:"li"},"assets/less")," to ",(0,n.yg)("inlineCode",{parentName:"li"},"app/less")),(0,n.yg)("li",{parentName:"ul"},'You have to move definition of less files pathes from build.js to "imports" - you have two options:',(0,n.yg)("ul",{parentName:"li"},(0,n.yg)("li",{parentName:"ul"},"import less files per component"),(0,n.yg)("li",{parentName:"ul"},"import root less file e.g. in main.js and use glob pattern to import other less files similar like it was in build.js"))),(0,n.yg)("li",{parentName:"ul"},"app/less/globals.less - this file is prepending to every less file so that you can import here variables, mixins, etc."),(0,n.yg)("li",{parentName:"ul"},"strictMaths is enabled")),(0,n.yg)("h2",{id:"tests"},"Tests"),(0,n.yg)("p",null,"Add ",(0,n.yg)("inlineCode",{parentName:"p"},"@swc/jest")," dependency.\nAdd ",(0,n.yg)("inlineCode",{parentName:"p"},"identity-obj-proxy")," for css/less in jest.\nReplace ",(0,n.yg)("inlineCode",{parentName:"p"},"enzyme-adapter-react-16")," with ",(0,n.yg)("inlineCode",{parentName:"p"},"@cfaester/enzyme-adapter-react-18"),"."),(0,n.yg)("h2",{id:"other-changes"},"Other changes"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"Prepared for typescript")),(0,n.yg)("h2",{id:"deleted-packages"},"Deleted packages"),(0,n.yg)("p",null,"You can remove following packages:"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"@ima/react-hooks - functionality moved to @ima/react-page-renderer"),(0,n.yg)("li",{parentName:"ul"},"@ima/plugin-less-constants moved to @ima/cli-plugin-less-constants"),(0,n.yg)("li",{parentName:"ul"},"@ima/plugin-hot-reload"),(0,n.yg)("li",{parentName:"ul"},"@ima/plugin-websocket"),(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-task-loader"),(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-tasks")),(0,n.yg)("h2",{id:"imajs-plugins"},"IMA.js Plugins"),(0,n.yg)("p",null,"All IMA.js plugins need to be updated to the latest version. Older versions won't work."))}g.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[9541],{5680:(e,r,t)=>{t.d(r,{xA:()=>p,yg:()=>g});var a=t(6540);function n(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}function l(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);r&&(a=a.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var r=1;r=0||(n[t]=e[t]);return n}(e,r);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(n[t]=e[t])}return n}var s=a.createContext({}),u=function(e){var r=a.useContext(s),t=r;return e&&(t="function"==typeof e?e(r):i(i({},r),e)),t},p=function(e){var r=u(e.components);return a.createElement(s.Provider,{value:r},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var r=e.children;return a.createElement(a.Fragment,{},r)}},m=a.forwardRef((function(e,r){var t=e.components,n=e.mdxType,l=e.originalType,s=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),c=u(t),m=n,g=c["".concat(s,".").concat(m)]||c[m]||d[m]||l;return t?a.createElement(g,i(i({ref:r},p),{},{components:t})):a.createElement(g,i({ref:r},p))}));function g(e,r){var t=arguments,n=r&&r.mdxType;if("string"==typeof e||n){var l=t.length,i=new Array(l);i[0]=m;var o={};for(var s in r)hasOwnProperty.call(r,s)&&(o[s]=r[s]);o.originalType=e,o[c]="string"==typeof e?e:n,i[1]=o;for(var u=2;u{t.d(r,{A:()=>i});var a=t(6540),n=t(8017);const l={tabItem:"tabItem_Ymn6"};function i(e){let{children:r,hidden:t,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,n.A)(l.tabItem,i),hidden:t},r)}},1253:(e,r,t)=>{t.d(r,{A:()=>j});var a=t(8102),n=t(6540),l=t(8017),i=t(3104),o=t(9519),s=t(7485),u=t(1682),p=t(9466);function c(e){return function(e){return n.Children.map(e,(e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:r}=e;return!!r&&"object"==typeof r&&"value"in r}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:r,label:t,attributes:a,default:n}}=e;return{value:r,label:t,attributes:a,default:n}}))}function d(e){const{values:r,children:t}=e;return(0,n.useMemo)((()=>{const e=r??c(t);return function(e){const r=(0,u.X)(e,((e,r)=>e.value===r.value));if(r.length>0)throw new Error(`Docusaurus error: Duplicate values "${r.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[r,t])}function m(e){let{value:r,tabValues:t}=e;return t.some((e=>e.value===r))}function g(e){let{queryString:r=!1,groupId:t}=e;const a=(0,o.W6)(),l=function(e){let{queryString:r=!1,groupId:t}=e;if("string"==typeof r)return r;if(!1===r)return null;if(!0===r&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:r,groupId:t});return[(0,s.aZ)(l),(0,n.useCallback)((e=>{if(!l)return;const r=new URLSearchParams(a.location.search);r.set(l,e),a.replace({...a.location,search:r.toString()})}),[l,a])]}function y(e){const{defaultValue:r,queryString:t=!1,groupId:a}=e,l=d(e),[i,o]=(0,n.useState)((()=>function(e){let{defaultValue:r,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(r){if(!m({value:r,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${r}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return r}const a=t.find((e=>e.default))??t[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:r,tabValues:l}))),[s,u]=g({queryString:t,groupId:a}),[c,y]=function(e){let{groupId:r}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(r),[a,l]=(0,p.Dv)(t);return[a,(0,n.useCallback)((e=>{t&&l.set(e)}),[t,l])]}({groupId:a}),v=(()=>{const e=s??c;return m({value:e,tabValues:l})?e:null})();(0,n.useLayoutEffect)((()=>{v&&o(v)}),[v]);return{selectedValue:i,selectValue:(0,n.useCallback)((e=>{if(!m({value:e,tabValues:l}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),y(e)}),[u,y,l]),tabValues:l}}var v=t(2303);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function b(e){let{className:r,block:t,selectedValue:o,selectValue:s,tabValues:u}=e;const p=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const r=e.currentTarget,t=p.indexOf(r),a=u[t].value;a!==o&&(c(r),s(a))},m=e=>{let r=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=p.indexOf(e.currentTarget)+1;r=p[t]??p[0];break}case"ArrowLeft":{const t=p.indexOf(e.currentTarget)-1;r=p[t]??p[p.length-1];break}}r?.focus()};return n.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,l.A)("tabs",{"tabs--block":t},r)},u.map((e=>{let{value:r,label:t,attributes:i}=e;return n.createElement("li",(0,a.A)({role:"tab",tabIndex:o===r?0:-1,"aria-selected":o===r,key:r,ref:e=>p.push(e),onKeyDown:m,onClick:d},i,{className:(0,l.A)("tabs__item",f.tabItem,i?.className,{"tabs__item--active":o===r})}),t??r)})))}function h(e){let{lazy:r,children:t,selectedValue:a}=e;const l=(Array.isArray(t)?t:[t]).filter(Boolean);if(r){const e=l.find((e=>e.props.value===a));return e?(0,n.cloneElement)(e,{className:"margin-top--md"}):null}return n.createElement("div",{className:"margin-top--md"},l.map(((e,r)=>(0,n.cloneElement)(e,{key:r,hidden:e.props.value!==a}))))}function N(e){const r=y(e);return n.createElement("div",{className:(0,l.A)("tabs-container",f.tabList)},n.createElement(b,(0,a.A)({},e,r)),n.createElement(h,(0,a.A)({},e,r)))}function j(e){const r=(0,v.A)();return n.createElement(N,(0,a.A)({key:String(r)},e))}},8809:(e,r,t)=>{t.r(r),t.d(r,{assets:()=>p,contentTitle:()=>s,default:()=>g,frontMatter:()=>o,metadata:()=>u,toc:()=>c});var a=t(8102),n=(t(6540),t(5680)),l=t(1253),i=t(6185);const o={title:"Migration 18.0.0",description:"Migration > Migration to version 18.0.0"},s=void 0,u={unversionedId:"migration/migration-18.0.0",id:"migration/migration-18.0.0",title:"Migration 18.0.0",description:"Migration > Migration to version 18.0.0",source:"@site/../docs/migration/migration-18.0.0.md",sourceDirName:"migration",slug:"/migration/migration-18.0.0",permalink:"/migration/migration-18.0.0",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/migration/migration-18.0.0.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Migration 18.0.0",description:"Migration > Migration to version 18.0.0"},sidebar:"docs",previous:{title:"Migration 17.0.0",permalink:"/migration/migration-17.0.0"},next:{title:"Migration 19.0.0",permalink:"/migration/migration-19.0.0"}},p={},c=[{value:"Update requirements",id:"update-requirements",level:2},{value:"Moved from gulp to webpack",id:"moved-from-gulp-to-webpack",level:2},{value:"Change scripts in package.json",id:"change-scripts-in-packagejson",level:3},{value:"Update settings.js",id:"update-settingsjs",level:3},{value:"Remove gulp specific things",id:"remove-gulp-specific-things",level:3},{value:"Removed build.js, optionally add ima.config.js file to root",id:"removed-buildjs-optionally-add-imaconfigjs-file-to-root",level:3},{value:"Moved from babel to swc",id:"moved-from-babel-to-swc",level:2},{value:"New React-page-renderer",id:"new-react-page-renderer",level:2},{value:"Update EventBus",id:"update-eventbus",level:3},{value:"Update DocumentView",id:"update-documentview",level:2},{value:"Update Server",id:"update-server",level:2},{value:"Split server.js -> server.js and app.js",id:"split-serverjs---serverjs-and-appjs",level:3},{value:"Server changes",id:"server-changes",level:3},{value:"Move environment.js file",id:"move-environmentjs-file",level:3},{value:"Templates",id:"templates",level:3},{value:"Update DocumentView",id:"update-documentview-1",level:3},{value:"Assets => app/public",id:"assets--apppublic",level:2},{value:"Styles",id:"styles",level:2},{value:"Tests",id:"tests",level:2},{value:"Other changes",id:"other-changes",level:2},{value:"Deleted packages",id:"deleted-packages",level:2},{value:"IMA.js Plugins",id:"imajs-plugins",level:2}],d={toc:c},m="wrapper";function g(e){let{components:r,...t}=e;return(0,n.yg)(m,(0,a.A)({},d,t,{components:r,mdxType:"MDXLayout"}),(0,n.yg)("p",null,"IMA.js brings few major breaking changes. For more information read below."),(0,n.yg)("h2",{id:"update-requirements"},"Update requirements"),(0,n.yg)("p",null,"IMA.js v18 requires node >= 18, npm >= 8 and react 18."),(0,n.yg)("h2",{id:"moved-from-gulp-to-webpack"},"Moved from gulp to webpack"),(0,n.yg)("p",null,"You can remove gulp things. There is new @ima/cli plugin for helping with webpack.\nFrom now, you have to import everything you want to be present in your bundle (that's how webpack works)."),(0,n.yg)("h3",{id:"change-scripts-in-packagejson"},"Change scripts in package.json"),(0,n.yg)("p",null,"There is new @ima/cli used in scripts instead of gulp."),(0,n.yg)("p",null,(0,n.yg)("strong",{parentName:"p"},"Example:")),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},'...\n "scripts": {\n "test": "jest",\n "lint": "eslint \'./**/*.{js,jsx,ts,tsx}\'",\n "dev": "ima dev",\n "build": "NODE_ENV=production ima build",\n "start": "NODE_ENV=production node server/server.js"\n},\n...\n')),(0,n.yg)("p",null,"Remove ",(0,n.yg)("inlineCode",{parentName:"p"},'"main": "build/server.js"')," from ",(0,n.yg)("inlineCode",{parentName:"p"},"package.json")," too. (Server is not anymore in build/server.js.)"),(0,n.yg)("h3",{id:"update-settingsjs"},"Update settings.js"),(0,n.yg)("p",null,"Remove scripts and esScripts from $Page.$Render (IMA process this things now by manifest and contentVariables)."),(0,n.yg)("h3",{id:"remove-gulp-specific-things"},"Remove gulp specific things"),(0,n.yg)("p",null,"Dependencies:"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-task-loader"),(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-tasks")),(0,n.yg)("p",null,"Files:"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"gulpfile.js"),(0,n.yg)("li",{parentName:"ul"},"gulpConfig.js")),(0,n.yg)("h3",{id:"removed-buildjs-optionally-add-imaconfigjs-file-to-root"},"Removed build.js, optionally add ima.config.js file to root"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"look at ",(0,n.yg)("a",{parentName:"li",href:"../cli/ima-config-js"},"ima.config.js")," "),(0,n.yg)("li",{parentName:"ul"},"definition of languages moved from ",(0,n.yg)("inlineCode",{parentName:"li"},"build.js")," to ",(0,n.yg)("inlineCode",{parentName:"li"},"ima.config.js")),(0,n.yg)("li",{parentName:"ul"},"definition of less file pathes is not needed - see section Styles below")),(0,n.yg)("h2",{id:"moved-from-babel-to-swc"},"Moved from babel to swc"),(0,n.yg)("p",null,"You can remove @babel dependencies (except for eslint specific). "),(0,n.yg)("p",null,"Add ",(0,n.yg)("inlineCode",{parentName:"p"},"@swc/jest")," devDependency for tests."),(0,n.yg)("h2",{id:"new-react-page-renderer"},"New React-page-renderer"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"React-page-renderer moved to new package @ima/react-page-renderer ")),(0,n.yg)(l.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,n.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-bash"},"npm i @ima/react-page-renderer\n"))),(0,n.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add @ima/react-page-renderer\n"))),(0,n.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add @ima/react-page-renderer\n")))),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},(0,n.yg)("p",{parentName:"li"},"You can use codemod ",(0,n.yg)("inlineCode",{parentName:"p"},"npx @cns/web-plugins-codemods")," -> ima18: react page renderer imports")),(0,n.yg)("li",{parentName:"ul"},(0,n.yg)("p",{parentName:"li"},"Update DocumentView - use AbstractPureComponent from @ima/react-page-renderer instead of AbstractDocumentView"))),(0,n.yg)("h3",{id:"update-eventbus"},"Update EventBus"),(0,n.yg)("p",null,"You have to add target as the second argument for EventBus fire, listen/unlisten."),(0,n.yg)("h2",{id:"update-documentview"},"Update DocumentView"),(0,n.yg)("p",null,"Rewrite your DocumentView similar like in create-ima-app."),(0,n.yg)("h2",{id:"update-server"},"Update Server"),(0,n.yg)("p",null,"You have to add dependency to ",(0,n.yg)("inlineCode",{parentName:"p"},"error-to-json")," on your own. It was removed from @ima/server."),(0,n.yg)("p",null,"Replace"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"let errorToJSON = require('error-to-json');\n")),(0,n.yg)("p",null,"by"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"const errorToJSON = require('error-to-json').default;\n")),(0,n.yg)("h3",{id:"split-serverjs---serverjs-and-appjs"},"Split server.js -> server.js and app.js"),(0,n.yg)("p",null,"This change is optionally, but we use it in our create-ima-app."),(0,n.yg)("h3",{id:"server-changes"},"Server changes"),(0,n.yg)("p",null,"Remove:"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"'use strict';\n\nrequire('@ima/core/polyfill/imaLoader.js');\nrequire('@ima/core/polyfill/imaRunner.js');\n")),(0,n.yg)("p",null,"Replace this part:"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"let imaServer = require('@ima/server');\n\nlet clientApp = imaServer.clientApp;\nlet urlParser = imaServer.urlParser;\nlet environment = imaServer.environment;\nlet logger = imaServer.logger;\nlet cache = imaServer.cache;\n")),(0,n.yg)("p",null,"by"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"const imaServer = require('@ima/server')();\nconst { serverApp, urlParser, environment, logger, cache, memStaticProxy } =\n imaServer;\n\nrequire('@ima/react-page-renderer/hook/server')(imaServer);\n")),(0,n.yg)("p",null,"Replace clientApp.requestHandler by serverApp.requestHandlerMiddleware."),(0,n.yg)("p",null,"Remove staticErrorPage and replace errorHandler function by"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-js"},"function renderError(error, req, res, next) {\n serverApp\n .errorHandlerMiddleware(error, req, res, next)\n .then(response => {\n logger.error(response.error);\n })\n .catch(next);\n}\n")),(0,n.yg)("h3",{id:"move-environmentjs-file"},"Move environment.js file"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"File ",(0,n.yg)("inlineCode",{parentName:"li"},"app/environment.js")," was moved to location ",(0,n.yg)("inlineCode",{parentName:"li"},"/server/config/environment.js"),"\nThere was removed ",(0,n.yg)("strong",{parentName:"li"},"test")," env.")),(0,n.yg)("h3",{id:"templates"},"Templates"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"400, 500, spa templates are in ",(0,n.yg)("inlineCode",{parentName:"li"},"server/template")," (look at ",(0,n.yg)("a",{parentName:"li",href:"https://github.com/seznam/ima/tree/master/packages/create-ima-app/template/server/template"},"create-ima-app"),")")),(0,n.yg)("h3",{id:"update-documentview-1"},"Update DocumentView"),(0,n.yg)("p",null,"You can remove getAsyncScripts method and body content replace with:\n(You have to add $Page.$Render.masterElementId property to settings.js)"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre",className:"language-jsx"}," \n {'#{revivalCache}'}\n {'#{revivalSettings}'}\n {'#{runner}'}\n")),(0,n.yg)("p",null,"Instead of app css loading use:"),(0,n.yg)("pre",null,(0,n.yg)("code",{parentName:"pre"}," {'#{styles}'}\n")),(0,n.yg)("h2",{id:"assets--apppublic"},"Assets => app/public"),(0,n.yg)("p",null,"Everything from folder app/public is moved to build folder into static folder."),(0,n.yg)("h2",{id:"styles"},"Styles"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"Remove files mark as ",(0,n.yg)("inlineCode",{parentName:"li"},"FAKE FILE FOR GULP LESS")),(0,n.yg)("li",{parentName:"ul"},"Move less files from ",(0,n.yg)("inlineCode",{parentName:"li"},"assets/less")," to ",(0,n.yg)("inlineCode",{parentName:"li"},"app/less")),(0,n.yg)("li",{parentName:"ul"},'You have to move definition of less files pathes from build.js to "imports" - you have two options:',(0,n.yg)("ul",{parentName:"li"},(0,n.yg)("li",{parentName:"ul"},"import less files per component"),(0,n.yg)("li",{parentName:"ul"},"import root less file e.g. in main.js and use glob pattern to import other less files similar like it was in build.js"))),(0,n.yg)("li",{parentName:"ul"},"app/less/globals.less - this file is prepending to every less file so that you can import here variables, mixins, etc."),(0,n.yg)("li",{parentName:"ul"},"strictMaths is enabled")),(0,n.yg)("h2",{id:"tests"},"Tests"),(0,n.yg)("p",null,"Add ",(0,n.yg)("inlineCode",{parentName:"p"},"@swc/jest")," dependency.\nAdd ",(0,n.yg)("inlineCode",{parentName:"p"},"identity-obj-proxy")," for css/less in jest.\nReplace ",(0,n.yg)("inlineCode",{parentName:"p"},"enzyme-adapter-react-16")," with ",(0,n.yg)("inlineCode",{parentName:"p"},"@cfaester/enzyme-adapter-react-18"),"."),(0,n.yg)("h2",{id:"other-changes"},"Other changes"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"Prepared for typescript")),(0,n.yg)("h2",{id:"deleted-packages"},"Deleted packages"),(0,n.yg)("p",null,"You can remove following packages:"),(0,n.yg)("ul",null,(0,n.yg)("li",{parentName:"ul"},"@ima/react-hooks - functionality moved to @ima/react-page-renderer"),(0,n.yg)("li",{parentName:"ul"},"@ima/plugin-less-constants moved to @ima/cli-plugin-less-constants"),(0,n.yg)("li",{parentName:"ul"},"@ima/plugin-hot-reload"),(0,n.yg)("li",{parentName:"ul"},"@ima/plugin-websocket"),(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-task-loader"),(0,n.yg)("li",{parentName:"ul"},"@ima/gulp-tasks")),(0,n.yg)("h2",{id:"imajs-plugins"},"IMA.js Plugins"),(0,n.yg)("p",null,"All IMA.js plugins need to be updated to the latest version. Older versions won't work."))}g.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/21ee5e18.91ab26eb.js b/assets/js/21ee5e18.aea9c9ff.js similarity index 99% rename from assets/js/21ee5e18.91ab26eb.js rename to assets/js/21ee5e18.aea9c9ff.js index 829f41020..bc5936f3b 100644 --- a/assets/js/21ee5e18.91ab26eb.js +++ b/assets/js/21ee5e18.aea9c9ff.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[5547],{5680:(e,n,t)=>{t.d(n,{xA:()=>l,yg:()=>m});var r=t(6540);function i(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function a(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function o(e){for(var n=1;n=0||(i[t]=e[t]);return i}(e,n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(i[t]=e[t])}return i}var c=r.createContext({}),p=function(e){var n=r.useContext(c),t=n;return e&&(t="function"==typeof e?e(n):o(o({},n),e)),t},l=function(e){var n=p(e.components);return r.createElement(c.Provider,{value:n},e.children)},d="mdxType",g={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},u=r.forwardRef((function(e,n){var t=e.components,i=e.mdxType,a=e.originalType,c=e.parentName,l=s(e,["components","mdxType","originalType","parentName"]),d=p(t),u=i,m=d["".concat(c,".").concat(u)]||d[u]||g[u]||a;return t?r.createElement(m,o(o({ref:n},l),{},{components:t})):r.createElement(m,o({ref:n},l))}));function m(e,n){var t=arguments,i=n&&n.mdxType;if("string"==typeof e||i){var a=t.length,o=new Array(a);o[0]=u;var s={};for(var c in n)hasOwnProperty.call(n,c)&&(s[c]=n[c]);s.originalType=e,s[d]="string"==typeof e?e:i,o[1]=s;for(var p=2;p{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>g,frontMatter:()=>a,metadata:()=>s,toc:()=>p});var r=t(8102),i=(t(6540),t(5680));const a={title:"Object Container",description:"Basic features > Object Container and IMA.js dependency injection"},o=void 0,s={unversionedId:"basic-features/object-container",id:"basic-features/object-container",title:"Object Container",description:"Basic features > Object Container and IMA.js dependency injection",source:"@site/../docs/basic-features/object-container.md",sourceDirName:"basic-features",slug:"/basic-features/object-container",permalink:"/basic-features/object-container",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/object-container.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Object Container",description:"Basic features > Object Container and IMA.js dependency injection"},sidebar:"docs",previous:{title:"Extensions",permalink:"/basic-features/extensions"},next:{title:"Rendering process",permalink:"/basic-features/rendering-process"}},c={},p=[{value:"Automatic registration with Object Container",id:"automatic-registration-with-object-container",level:2},{value:"Manually registering dependencies",id:"manually-registering-dependencies",level:2},{value:"1. bind()",id:"1-bind",level:3},{value:"2. constant()",id:"2-constant",level:3},{value:"3. inject()",id:"3-inject",level:3},{value:"4. provide()",id:"4-provide",level:3},{value:"Obtaining dependencies",id:"obtaining-dependencies",level:2},{value:"1. Dependency Injection",id:"1-dependency-injection",level:3},{value:"Optional dependencies",id:"optional-dependencies",level:4},{value:"Spread dependencies",id:"spread-dependencies",level:4},{value:"2. get()",id:"2-get",level:3},{value:"3. create()",id:"3-create",level:3},{value:"Other methods",id:"other-methods",level:2}],l={toc:p},d="wrapper";function g(e){let{components:n,...t}=e;return(0,i.yg)(d,(0,r.A)({},l,t,{components:n,mdxType:"MDXLayout"}),(0,i.yg)("p",null,"The ",(0,i.yg)("strong",{parentName:"p"},"Object Container (OC)")," is an enhanced dependency injector with support for aliases and constants. It is sophisticated and registers everything it comes across but only if it actually matters."),(0,i.yg)("p",null,"By registering controllers and views the OC can simply follow your dependency tree and register everything you might possibly need. Below is a diagram of simple dependency tree."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre"},"app/config/routes.js\n\u251c\u2500 OrderController\n| \u251c\u2500 OrderService / OrderEntity\n| | \u2514\u2500 RestClient\n| | \u251c\u2500 $HttpAgent\n| | \u251c\u2500 $Cache\n| | \u2514\u2500 LinkGenerator\n| | \u2514\u2500 $Router\n| \u2514\u2500 UserService / UserEntity\n| \u2514\u2500 RestClient\n| \u251c\u2500 $HttpAgent\n| \u251c\u2500 $Cache\n| \u2514\u2500 LinkGenerator\n| \u2514\u2500 $Router\n\u2514\u2500 UserController\n \u251c\u2500 ...\n \u2514\u2500 ...\n")),(0,i.yg)("h2",{id:"automatic-registration-with-object-container"},"Automatic registration with Object Container"),(0,i.yg)("p",null,"Every class that defines static property ",(0,i.yg)("inlineCode",{parentName:"p"},"$dependencies")," which exports array of dependencies is automatically registered to ",(0,i.yg)("inlineCode",{parentName:"p"},"oc")," and instanced when it is used (this can happen lazily upon first usage)."),(0,i.yg)("h2",{id:"manually-registering-dependencies"},"Manually registering dependencies"),(0,i.yg)("p",null,"Since the OC cannot discover everything and doesn't know about interfaces you can register your dependencies in a file ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/bind.js"),".\nThis file contains a function that receives the namespace register\n",(0,i.yg)("em",{parentName:"p"},"(deprecated)"),", OC instance and a config object."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\nexport let init = (ns, oc, config) => {\n // Register stuff here\n}\n")),(0,i.yg)("p",null,"OC handles ",(0,i.yg)("em",{parentName:"p"},"instances")," of registered dependencies. When registering a class, be aware that its static methods and properties won't be available through OC."),(0,i.yg)("p",null,"Below is list of methods that the OC provides to register your dependencies."),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:")," Every method returns the OC itself so you can chain them together.")),(0,i.yg)("h3",{id:"1-bind"},"1. ",(0,i.yg)("inlineCode",{parentName:"h3"},"bind()")),(0,i.yg)("p",null,"Binds the specified class or factory function and dependencies to the\nspecified alias.\nThis allows to create new instances of the class or the\nfunction by referencing the alias. Same goes for specifying the class of\nthe function as a dependency."),(0,i.yg)("p",null,"Also note that the same class or function may be bound to several\naliases and each may use different dependencies."),(0,i.yg)("p",null,"The alias will use the current dependencies bound to the class if no\ndependencies are provided."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Binding custom router implementation and\n// UserAgent class from IMA.js user-agent plugin\n\nimport { UserAgent } from '@ima/plugin-useragent';\nimport { CustomRouter } from 'app/your-custom-overrides/Router';\n\nexport let init = (ns, oc, config) => {\n // Simple alias\n oc.bind('UserAgent', UserAgent);\n\n // Alias with dependencies\n // Override of the IMA.js router implementation\n oc.bind('$Router', CustomRouter, [\n '$PageManager', '$RouteFactory', '$Dispatcher', Window\n ]);\n\n // ...\n}\n")),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:")," The dollar-sign ",(0,i.yg)("inlineCode",{parentName:"p"},"$")," at the beginning of an alias marks IMA.js\ninternal component.")),(0,i.yg)("h3",{id:"2-constant"},"2. ",(0,i.yg)("inlineCode",{parentName:"h3"},"constant()")),(0,i.yg)("p",null,"Defines a new constant registered within the OC. Note that\nthis is the only way of passing ",(0,i.yg)("inlineCode",{parentName:"p"},"string")," values to constructors\nbecause the OC treats strings as class, interface, alias\nor constant names. Once the constant is defined it cannot be redefined."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Assigning API root URL to a constant that can be later used as a dependency\n// (for example in IMA.js RestAPI client)\n\nexport let init = (ns, oc, config) => {\n oc.constant('REST_API_ROOT_URL', config.api.url);\n}\n")),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:"),"\xa0Constants are not limited to primitive values but can also\ntake objects.")),(0,i.yg)("h3",{id:"3-inject"},"3. ",(0,i.yg)("inlineCode",{parentName:"h3"},"inject()")),(0,i.yg)("p",null,"Configures the object loader with the specified default dependencies for\nthe specified class."),(0,i.yg)("p",null,"New instances of the class created by the OC will receive the provided\ndependencies into constructor unless custom dependencies are provided."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Injecting the rest client.\n// Notice how we used the REST_API_ROOT_URL constant\n\nimport Cache from 'ima/cache/Cache';\nimport HttpAgent from 'ima/http/HttpAgent';\nimport SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';\nimport LinkGenerator from 'app/rest-client-impl/LinkGenerator';\n\nexport let init = (ns, oc, config) => {\n oc.inject(SimpleRestClient, [\n HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator\n ]);\n}\n")),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:")," For more information about the IMA.js REST Client see ",(0,i.yg)("a",{parentName:"p",href:"https://github.com/jurca/IMA-plugin-rest-client"},"IMA-plugin-rest-client")," repository.")),(0,i.yg)("h3",{id:"4-provide"},"4. ",(0,i.yg)("inlineCode",{parentName:"h3"},"provide()")),(0,i.yg)("p",null,"Configures the default implementation of the specified interface.\nWhen the interface is requested from the OC the default implementation\nis provided."),(0,i.yg)("p",null,"The implementation constructor will obtain the provided default\ndependencies or the dependencies provided to the ",(0,i.yg)("a",{parentName:"p",href:"#3-create"},(0,i.yg)("inlineCode",{parentName:"a"},"create()"))," method."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n//\n\nimport { AbstractRestClient } from 'ima-plugin-rest-client';\nimport SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';\n\nexport let init = (ns, oc, config) => {\n oc.provide(AbstractRestClient, SimpleRestClient);\n\n // We didn't specify any dependencies on purpose\n // they were set in the previous example.\n // Otherwise it would be like this:\n\n oc.provide(\n AbstractRestClient,\n SimpleRestClient,\n [\n HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator\n ]\n );\n}\n")),(0,i.yg)("h2",{id:"obtaining-dependencies"},"Obtaining dependencies"),(0,i.yg)("p",null,"In IMA.js application you can obtain dependencies using many different methods, where each one can be useful in different situation and environment."),(0,i.yg)("h3",{id:"1-dependency-injection"},"1. Dependency Injection"),(0,i.yg)("p",null,"Apart from defining dependencies manually in ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/bind.js")," can every class (discovered by the OC) define a static getter ",(0,i.yg)("inlineCode",{parentName:"p"},"$dependencies"),". This getter should return list of dependencies specified by a class constructor or a ",(0,i.yg)("inlineCode",{parentName:"p"},"string")," alias."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/order/OrderController.js\n//\n// OrderController is discovered by the OC\n// because it's registered in app/config/routes.js\n\nimport { AbstractController } from '@ima/core';\nimport OrderService from 'app/model/order/OrderService.js';\nimport UserService from 'app/model/user/UserService.js';\n\nexport default class OrderController extends AbstractController {\n\n static get $dependencies() {\n return [\n OrderService,\n UserService,\n '$Router'\n ];\n }\n\n // ...\n")),(0,i.yg)("p",null,"Once you've defined the dependencies the constructor of the class will receive their instances."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"}," constructor(orderService, userService, $router) {\n super();\n\n this._orderService = orderService;\n this._userService = userService;\n this._$router = $router;\n }\n\n // ...\n\n")),(0,i.yg)("h4",{id:"optional-dependencies"},"Optional dependencies"),(0,i.yg)("p",null,"Dependencies can also be defined as optional.\nIf those dependencies are present in the OC, the constructor of the class will receive their instances.\nOtherwise it will receive ",(0,i.yg)("inlineCode",{parentName:"p"},"undefined"),"."),(0,i.yg)("p",null,"To use optional dependency, prefix ",(0,i.yg)("inlineCode",{parentName:"p"},"?")," is added before the string alias or the dependency is wrapped in array, with option specifying if it's optional or not."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/order/OrderController.js\n//\n// OrderController using optional dependencies\n\nimport { AbstractController } from '@ima/core';\nimport OrderService from 'app/model/order/OrderService.js';\nimport UserService from 'app/model/user/UserService.js';\n\nexport default class OrderController extends AbstractController {\n\n static get $dependencies() {\n return [\n [OrderService, { optional: true }],\n [UserService, { optional: false }],\n '?$Settings.api.serverApiUrl'\n ];\n }\n\n // ...\n")),(0,i.yg)("h4",{id:"spread-dependencies"},"Spread dependencies"),(0,i.yg)("p",null,"Dependencies can be added to array registered in the OC. These dependencies can be then spread to the class constructor using spread operator ",(0,i.yg)("inlineCode",{parentName:"p"},"..."),"."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Creating array of dependencies\n\nimport OrderService from 'app/model/order/OrderService.js';\nimport UserService from 'app/model/user/UserService.js';\n\nexport let init = (ns, oc, config) => {\n oc.constant('$spreadDependencies', [OrderService, UserService]);\n}\n")),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/order/OrderController.js\n//\n// OrderController using spread dependencies\n\nimport { AbstractController } from '@ima/core';\n\nexport default class OrderController extends AbstractController {\n\n static get $dependencies() {\n return ['...$spreadDependencies'];\n }\n\n constructor(orderService, userService) {\n super();\n\n this._orderService = orderService;\n this._userService = userService;\n }\n\n // ...\n")),(0,i.yg)("p",null,"Spread and optional dependencies can be combined."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// ...\nstatic get $dependencies() {\n return ['...?$spreadDependencies'];\n}\n// ...\n")),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// ...\nstatic get $dependencies() {\n return [['...$spreadDependencies', { optional: true }]];\n}\n// ...\n")),(0,i.yg)("h3",{id:"2-get"},"2. ",(0,i.yg)("inlineCode",{parentName:"h3"},"get()")),(0,i.yg)("p",null,"Retrieves the ",(0,i.yg)("strong",{parentName:"p"},"shared instance")," or value of the specified constant, alias,\nclass or factory function, interface, or fully qualified namespace path\n(the method checks these in this order in case of a name clash)."),(0,i.yg)("p",null,"The instance or value is created lazily the first time it is requested."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"oc.get('REST_API_ROOT_URL');\noc.get('UserAgent');\noc.get(AbstractRestClient); // This returns instance of `SimpleRestClient` as we defined in the previous example\n")),(0,i.yg)("h3",{id:"3-create"},"3. ",(0,i.yg)("inlineCode",{parentName:"h3"},"create()")),(0,i.yg)("p",null,"Creates a ",(0,i.yg)("strong",{parentName:"p"},"new instance")," of the class or retrieves the value generated by\nthe factory function identified by the provided name, class, interface,\nor factory function, passing in the provided dependencies."),(0,i.yg)("p",null,"The method uses the dependencies specified when the class, interface or\nfactory function has been registered with the object container if no\ncustom dependencies are provided."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"import { Cache, HttpAgent } from '@ima/core';\nimport SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';\nimport LinkGenerator from 'app/rest-client-impl/LinkGenerator';\n\noc.create('UserAgent');\noc.create(\n SimpleRestClient,\n [\n HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator\n ]\n);\n")),(0,i.yg)("p",null,"The last two method are not used as much as the first one but can be\nuseful inside the ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/bind.js")," and ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/routes.js")),(0,i.yg)("h2",{id:"other-methods"},"Other methods"),(0,i.yg)("ul",null,(0,i.yg)("li",{parentName:"ul"},(0,i.yg)("inlineCode",{parentName:"li"},"has()")," returns ",(0,i.yg)("inlineCode",{parentName:"li"},"true")," if the specified object, class or resource is registered\nwithin the OC.")),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"if (oc.has('UserAgent') && oc.get('UserAgent').isMobile()) {\n // Register conditional stuff here...\n}\n")),(0,i.yg)("ul",null,(0,i.yg)("li",{parentName:"ul"},(0,i.yg)("inlineCode",{parentName:"li"},"getConstructorOf()")," returns the class constructor function of the specified class or alias.")))}g.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[5547],{5680:(e,n,t)=>{t.d(n,{xA:()=>l,yg:()=>m});var r=t(6540);function i(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function a(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function o(e){for(var n=1;n=0||(i[t]=e[t]);return i}(e,n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(i[t]=e[t])}return i}var c=r.createContext({}),p=function(e){var n=r.useContext(c),t=n;return e&&(t="function"==typeof e?e(n):o(o({},n),e)),t},l=function(e){var n=p(e.components);return r.createElement(c.Provider,{value:n},e.children)},d="mdxType",g={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},u=r.forwardRef((function(e,n){var t=e.components,i=e.mdxType,a=e.originalType,c=e.parentName,l=s(e,["components","mdxType","originalType","parentName"]),d=p(t),u=i,m=d["".concat(c,".").concat(u)]||d[u]||g[u]||a;return t?r.createElement(m,o(o({ref:n},l),{},{components:t})):r.createElement(m,o({ref:n},l))}));function m(e,n){var t=arguments,i=n&&n.mdxType;if("string"==typeof e||i){var a=t.length,o=new Array(a);o[0]=u;var s={};for(var c in n)hasOwnProperty.call(n,c)&&(s[c]=n[c]);s.originalType=e,s[d]="string"==typeof e?e:i,o[1]=s;for(var p=2;p{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>g,frontMatter:()=>a,metadata:()=>s,toc:()=>p});var r=t(8102),i=(t(6540),t(5680));const a={title:"Object Container",description:"Basic features > Object Container and IMA.js dependency injection"},o=void 0,s={unversionedId:"basic-features/object-container",id:"basic-features/object-container",title:"Object Container",description:"Basic features > Object Container and IMA.js dependency injection",source:"@site/../docs/basic-features/object-container.md",sourceDirName:"basic-features",slug:"/basic-features/object-container",permalink:"/basic-features/object-container",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/object-container.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Object Container",description:"Basic features > Object Container and IMA.js dependency injection"},sidebar:"docs",previous:{title:"Extensions",permalink:"/basic-features/extensions"},next:{title:"Rendering process",permalink:"/basic-features/rendering-process"}},c={},p=[{value:"Automatic registration with Object Container",id:"automatic-registration-with-object-container",level:2},{value:"Manually registering dependencies",id:"manually-registering-dependencies",level:2},{value:"1. bind()",id:"1-bind",level:3},{value:"2. constant()",id:"2-constant",level:3},{value:"3. inject()",id:"3-inject",level:3},{value:"4. provide()",id:"4-provide",level:3},{value:"Obtaining dependencies",id:"obtaining-dependencies",level:2},{value:"1. Dependency Injection",id:"1-dependency-injection",level:3},{value:"Optional dependencies",id:"optional-dependencies",level:4},{value:"Spread dependencies",id:"spread-dependencies",level:4},{value:"2. get()",id:"2-get",level:3},{value:"3. create()",id:"3-create",level:3},{value:"Other methods",id:"other-methods",level:2}],l={toc:p},d="wrapper";function g(e){let{components:n,...t}=e;return(0,i.yg)(d,(0,r.A)({},l,t,{components:n,mdxType:"MDXLayout"}),(0,i.yg)("p",null,"The ",(0,i.yg)("strong",{parentName:"p"},"Object Container (OC)")," is an enhanced dependency injector with support for aliases and constants. It is sophisticated and registers everything it comes across but only if it actually matters."),(0,i.yg)("p",null,"By registering controllers and views the OC can simply follow your dependency tree and register everything you might possibly need. Below is a diagram of simple dependency tree."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre"},"app/config/routes.js\n\u251c\u2500 OrderController\n| \u251c\u2500 OrderService / OrderEntity\n| | \u2514\u2500 RestClient\n| | \u251c\u2500 $HttpAgent\n| | \u251c\u2500 $Cache\n| | \u2514\u2500 LinkGenerator\n| | \u2514\u2500 $Router\n| \u2514\u2500 UserService / UserEntity\n| \u2514\u2500 RestClient\n| \u251c\u2500 $HttpAgent\n| \u251c\u2500 $Cache\n| \u2514\u2500 LinkGenerator\n| \u2514\u2500 $Router\n\u2514\u2500 UserController\n \u251c\u2500 ...\n \u2514\u2500 ...\n")),(0,i.yg)("h2",{id:"automatic-registration-with-object-container"},"Automatic registration with Object Container"),(0,i.yg)("p",null,"Every class that defines static property ",(0,i.yg)("inlineCode",{parentName:"p"},"$dependencies")," which exports array of dependencies is automatically registered to ",(0,i.yg)("inlineCode",{parentName:"p"},"oc")," and instanced when it is used (this can happen lazily upon first usage)."),(0,i.yg)("h2",{id:"manually-registering-dependencies"},"Manually registering dependencies"),(0,i.yg)("p",null,"Since the OC cannot discover everything and doesn't know about interfaces you can register your dependencies in a file ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/bind.js"),".\nThis file contains a function that receives the namespace register\n",(0,i.yg)("em",{parentName:"p"},"(deprecated)"),", OC instance and a config object."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\nexport let init = (ns, oc, config) => {\n // Register stuff here\n}\n")),(0,i.yg)("p",null,"OC handles ",(0,i.yg)("em",{parentName:"p"},"instances")," of registered dependencies. When registering a class, be aware that its static methods and properties won't be available through OC."),(0,i.yg)("p",null,"Below is list of methods that the OC provides to register your dependencies."),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:")," Every method returns the OC itself so you can chain them together.")),(0,i.yg)("h3",{id:"1-bind"},"1. ",(0,i.yg)("inlineCode",{parentName:"h3"},"bind()")),(0,i.yg)("p",null,"Binds the specified class or factory function and dependencies to the\nspecified alias.\nThis allows to create new instances of the class or the\nfunction by referencing the alias. Same goes for specifying the class of\nthe function as a dependency."),(0,i.yg)("p",null,"Also note that the same class or function may be bound to several\naliases and each may use different dependencies."),(0,i.yg)("p",null,"The alias will use the current dependencies bound to the class if no\ndependencies are provided."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Binding custom router implementation and\n// UserAgent class from IMA.js user-agent plugin\n\nimport { UserAgent } from '@ima/plugin-useragent';\nimport { CustomRouter } from 'app/your-custom-overrides/Router';\n\nexport let init = (ns, oc, config) => {\n // Simple alias\n oc.bind('UserAgent', UserAgent);\n\n // Alias with dependencies\n // Override of the IMA.js router implementation\n oc.bind('$Router', CustomRouter, [\n '$PageManager', '$RouteFactory', '$Dispatcher', Window\n ]);\n\n // ...\n}\n")),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:")," The dollar-sign ",(0,i.yg)("inlineCode",{parentName:"p"},"$")," at the beginning of an alias marks IMA.js\ninternal component.")),(0,i.yg)("h3",{id:"2-constant"},"2. ",(0,i.yg)("inlineCode",{parentName:"h3"},"constant()")),(0,i.yg)("p",null,"Defines a new constant registered within the OC. Note that\nthis is the only way of passing ",(0,i.yg)("inlineCode",{parentName:"p"},"string")," values to constructors\nbecause the OC treats strings as class, interface, alias\nor constant names. Once the constant is defined it cannot be redefined."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Assigning API root URL to a constant that can be later used as a dependency\n// (for example in IMA.js RestAPI client)\n\nexport let init = (ns, oc, config) => {\n oc.constant('REST_API_ROOT_URL', config.api.url);\n}\n")),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:"),"\xa0Constants are not limited to primitive values but can also\ntake objects.")),(0,i.yg)("h3",{id:"3-inject"},"3. ",(0,i.yg)("inlineCode",{parentName:"h3"},"inject()")),(0,i.yg)("p",null,"Configures the object loader with the specified default dependencies for\nthe specified class."),(0,i.yg)("p",null,"New instances of the class created by the OC will receive the provided\ndependencies into constructor unless custom dependencies are provided."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Injecting the rest client.\n// Notice how we used the REST_API_ROOT_URL constant\n\nimport Cache from 'ima/cache/Cache';\nimport HttpAgent from 'ima/http/HttpAgent';\nimport SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';\nimport LinkGenerator from 'app/rest-client-impl/LinkGenerator';\n\nexport let init = (ns, oc, config) => {\n oc.inject(SimpleRestClient, [\n HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator\n ]);\n}\n")),(0,i.yg)("blockquote",null,(0,i.yg)("p",{parentName:"blockquote"},(0,i.yg)("strong",{parentName:"p"},"Note:")," For more information about the IMA.js REST Client see ",(0,i.yg)("a",{parentName:"p",href:"https://github.com/jurca/IMA-plugin-rest-client"},"IMA-plugin-rest-client")," repository.")),(0,i.yg)("h3",{id:"4-provide"},"4. ",(0,i.yg)("inlineCode",{parentName:"h3"},"provide()")),(0,i.yg)("p",null,"Configures the default implementation of the specified interface.\nWhen the interface is requested from the OC the default implementation\nis provided."),(0,i.yg)("p",null,"The implementation constructor will obtain the provided default\ndependencies or the dependencies provided to the ",(0,i.yg)("a",{parentName:"p",href:"#3-create"},(0,i.yg)("inlineCode",{parentName:"a"},"create()"))," method."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n//\n\nimport { AbstractRestClient } from 'ima-plugin-rest-client';\nimport SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';\n\nexport let init = (ns, oc, config) => {\n oc.provide(AbstractRestClient, SimpleRestClient);\n\n // We didn't specify any dependencies on purpose\n // they were set in the previous example.\n // Otherwise it would be like this:\n\n oc.provide(\n AbstractRestClient,\n SimpleRestClient,\n [\n HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator\n ]\n );\n}\n")),(0,i.yg)("h2",{id:"obtaining-dependencies"},"Obtaining dependencies"),(0,i.yg)("p",null,"In IMA.js application you can obtain dependencies using many different methods, where each one can be useful in different situation and environment."),(0,i.yg)("h3",{id:"1-dependency-injection"},"1. Dependency Injection"),(0,i.yg)("p",null,"Apart from defining dependencies manually in ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/bind.js")," can every class (discovered by the OC) define a static getter ",(0,i.yg)("inlineCode",{parentName:"p"},"$dependencies"),". This getter should return list of dependencies specified by a class constructor or a ",(0,i.yg)("inlineCode",{parentName:"p"},"string")," alias."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/order/OrderController.js\n//\n// OrderController is discovered by the OC\n// because it's registered in app/config/routes.js\n\nimport { AbstractController } from '@ima/core';\nimport OrderService from 'app/model/order/OrderService.js';\nimport UserService from 'app/model/user/UserService.js';\n\nexport default class OrderController extends AbstractController {\n\n static get $dependencies() {\n return [\n OrderService,\n UserService,\n '$Router'\n ];\n }\n\n // ...\n")),(0,i.yg)("p",null,"Once you've defined the dependencies the constructor of the class will receive their instances."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"}," constructor(orderService, userService, $router) {\n super();\n\n this._orderService = orderService;\n this._userService = userService;\n this._$router = $router;\n }\n\n // ...\n\n")),(0,i.yg)("h4",{id:"optional-dependencies"},"Optional dependencies"),(0,i.yg)("p",null,"Dependencies can also be defined as optional.\nIf those dependencies are present in the OC, the constructor of the class will receive their instances.\nOtherwise it will receive ",(0,i.yg)("inlineCode",{parentName:"p"},"undefined"),"."),(0,i.yg)("p",null,"To use optional dependency, prefix ",(0,i.yg)("inlineCode",{parentName:"p"},"?")," is added before the string alias or the dependency is wrapped in array, with option specifying if it's optional or not."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/order/OrderController.js\n//\n// OrderController using optional dependencies\n\nimport { AbstractController } from '@ima/core';\nimport OrderService from 'app/model/order/OrderService.js';\nimport UserService from 'app/model/user/UserService.js';\n\nexport default class OrderController extends AbstractController {\n\n static get $dependencies() {\n return [\n [OrderService, { optional: true }],\n [UserService, { optional: false }],\n '?$Settings.api.serverApiUrl'\n ];\n }\n\n // ...\n")),(0,i.yg)("h4",{id:"spread-dependencies"},"Spread dependencies"),(0,i.yg)("p",null,"Dependencies can be added to array registered in the OC. These dependencies can be then spread to the class constructor using spread operator ",(0,i.yg)("inlineCode",{parentName:"p"},"..."),"."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/config/bind.js\n//\n// Creating array of dependencies\n\nimport OrderService from 'app/model/order/OrderService.js';\nimport UserService from 'app/model/user/UserService.js';\n\nexport let init = (ns, oc, config) => {\n oc.constant('$spreadDependencies', [OrderService, UserService]);\n}\n")),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/order/OrderController.js\n//\n// OrderController using spread dependencies\n\nimport { AbstractController } from '@ima/core';\n\nexport default class OrderController extends AbstractController {\n\n static get $dependencies() {\n return ['...$spreadDependencies'];\n }\n\n constructor(orderService, userService) {\n super();\n\n this._orderService = orderService;\n this._userService = userService;\n }\n\n // ...\n")),(0,i.yg)("p",null,"Spread and optional dependencies can be combined."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// ...\nstatic get $dependencies() {\n return ['...?$spreadDependencies'];\n}\n// ...\n")),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"// ...\nstatic get $dependencies() {\n return [['...$spreadDependencies', { optional: true }]];\n}\n// ...\n")),(0,i.yg)("h3",{id:"2-get"},"2. ",(0,i.yg)("inlineCode",{parentName:"h3"},"get()")),(0,i.yg)("p",null,"Retrieves the ",(0,i.yg)("strong",{parentName:"p"},"shared instance")," or value of the specified constant, alias,\nclass or factory function, interface, or fully qualified namespace path\n(the method checks these in this order in case of a name clash)."),(0,i.yg)("p",null,"The instance or value is created lazily the first time it is requested."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"oc.get('REST_API_ROOT_URL');\noc.get('UserAgent');\noc.get(AbstractRestClient); // This returns instance of `SimpleRestClient` as we defined in the previous example\n")),(0,i.yg)("h3",{id:"3-create"},"3. ",(0,i.yg)("inlineCode",{parentName:"h3"},"create()")),(0,i.yg)("p",null,"Creates a ",(0,i.yg)("strong",{parentName:"p"},"new instance")," of the class or retrieves the value generated by\nthe factory function identified by the provided name, class, interface,\nor factory function, passing in the provided dependencies."),(0,i.yg)("p",null,"The method uses the dependencies specified when the class, interface or\nfactory function has been registered with the object container if no\ncustom dependencies are provided."),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"import { Cache, HttpAgent } from '@ima/core';\nimport SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';\nimport LinkGenerator from 'app/rest-client-impl/LinkGenerator';\n\noc.create('UserAgent');\noc.create(\n SimpleRestClient,\n [\n HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator\n ]\n);\n")),(0,i.yg)("p",null,"The last two method are not used as much as the first one but can be\nuseful inside the ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/bind.js")," and ",(0,i.yg)("inlineCode",{parentName:"p"},"app/config/routes.js")),(0,i.yg)("h2",{id:"other-methods"},"Other methods"),(0,i.yg)("ul",null,(0,i.yg)("li",{parentName:"ul"},(0,i.yg)("inlineCode",{parentName:"li"},"has()")," returns ",(0,i.yg)("inlineCode",{parentName:"li"},"true")," if the specified object, class or resource is registered\nwithin the OC.")),(0,i.yg)("pre",null,(0,i.yg)("code",{parentName:"pre",className:"language-javascript"},"if (oc.has('UserAgent') && oc.get('UserAgent').isMobile()) {\n // Register conditional stuff here...\n}\n")),(0,i.yg)("ul",null,(0,i.yg)("li",{parentName:"ul"},(0,i.yg)("inlineCode",{parentName:"li"},"getConstructorOf()")," returns the class constructor function of the specified class or alias.")))}g.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/23f47465.6b3ab06b.js b/assets/js/23f47465.45b4733c.js similarity index 99% rename from assets/js/23f47465.6b3ab06b.js rename to assets/js/23f47465.45b4733c.js index da39bd873..5e4101f06 100644 --- a/assets/js/23f47465.6b3ab06b.js +++ b/assets/js/23f47465.45b4733c.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[6098],{5680:(e,n,a)=>{a.d(n,{xA:()=>u,yg:()=>g});var t=a(6540);function o(e,n,a){return n in e?Object.defineProperty(e,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[n]=a,e}function l(e,n){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);n&&(t=t.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),a.push.apply(a,t)}return a}function r(e){for(var n=1;n=0||(o[a]=e[a]);return o}(e,n);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(o[a]=e[a])}return o}var s=t.createContext({}),p=function(e){var n=t.useContext(s),a=n;return e&&(a="function"==typeof e?e(n):r(r({},n),e)),a},u=function(e){var n=p(e.components);return t.createElement(s.Provider,{value:n},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return t.createElement(t.Fragment,{},n)}},m=t.forwardRef((function(e,n){var a=e.components,o=e.mdxType,l=e.originalType,s=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),c=p(a),m=o,g=c["".concat(s,".").concat(m)]||c[m]||d[m]||l;return a?t.createElement(g,r(r({ref:n},u),{},{components:a})):t.createElement(g,r({ref:n},u))}));function g(e,n){var a=arguments,o=n&&n.mdxType;if("string"==typeof e||o){var l=a.length,r=new Array(l);r[0]=m;var i={};for(var s in n)hasOwnProperty.call(n,s)&&(i[s]=n[s]);i.originalType=e,i[c]="string"==typeof e?e:o,r[1]=i;for(var p=2;p{a.d(n,{A:()=>r});var t=a(6540),o=a(8017);const l={tabItem:"tabItem_Ymn6"};function r(e){let{children:n,hidden:a,className:r}=e;return t.createElement("div",{role:"tabpanel",className:(0,o.A)(l.tabItem,r),hidden:a},n)}},1253:(e,n,a)=>{a.d(n,{A:()=>N});var t=a(8102),o=a(6540),l=a(8017),r=a(3104),i=a(9519),s=a(7485),p=a(1682),u=a(9466);function c(e){return function(e){return o.Children.map(e,(e=>{if(!e||(0,o.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:n,label:a,attributes:t,default:o}}=e;return{value:n,label:a,attributes:t,default:o}}))}function d(e){const{values:n,children:a}=e;return(0,o.useMemo)((()=>{const e=n??c(a);return function(e){const n=(0,p.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,a])}function m(e){let{value:n,tabValues:a}=e;return a.some((e=>e.value===n))}function g(e){let{queryString:n=!1,groupId:a}=e;const t=(0,i.W6)(),l=function(e){let{queryString:n=!1,groupId:a}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:n,groupId:a});return[(0,s.aZ)(l),(0,o.useCallback)((e=>{if(!l)return;const n=new URLSearchParams(t.location.search);n.set(l,e),t.replace({...t.location,search:n.toString()})}),[l,t])]}function y(e){const{defaultValue:n,queryString:a=!1,groupId:t}=e,l=d(e),[r,i]=(0,o.useState)((()=>function(e){let{defaultValue:n,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=a.find((e=>e.default))??a[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:l}))),[s,p]=g({queryString:a,groupId:t}),[c,y]=function(e){let{groupId:n}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,l]=(0,u.Dv)(a);return[t,(0,o.useCallback)((e=>{a&&l.set(e)}),[a,l])]}({groupId:t}),b=(()=>{const e=s??c;return m({value:e,tabValues:l})?e:null})();(0,o.useLayoutEffect)((()=>{b&&i(b)}),[b]);return{selectedValue:r,selectValue:(0,o.useCallback)((e=>{if(!m({value:e,tabValues:l}))throw new Error(`Can't select invalid tab value=${e}`);i(e),p(e),y(e)}),[p,y,l]),tabValues:l}}var b=a(2303);const h={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function f(e){let{className:n,block:a,selectedValue:i,selectValue:s,tabValues:p}=e;const u=[],{blockElementScrollPositionUntilNextRender:c}=(0,r.a_)(),d=e=>{const n=e.currentTarget,a=u.indexOf(n),t=p[a].value;t!==i&&(c(n),s(t))},m=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=u.indexOf(e.currentTarget)+1;n=u[a]??u[0];break}case"ArrowLeft":{const a=u.indexOf(e.currentTarget)-1;n=u[a]??u[u.length-1];break}}n?.focus()};return o.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,l.A)("tabs",{"tabs--block":a},n)},p.map((e=>{let{value:n,label:a,attributes:r}=e;return o.createElement("li",(0,t.A)({role:"tab",tabIndex:i===n?0:-1,"aria-selected":i===n,key:n,ref:e=>u.push(e),onKeyDown:m,onClick:d},r,{className:(0,l.A)("tabs__item",h.tabItem,r?.className,{"tabs__item--active":i===n})}),a??n)})))}function v(e){let{lazy:n,children:a,selectedValue:t}=e;const l=(Array.isArray(a)?a:[a]).filter(Boolean);if(n){const e=l.find((e=>e.props.value===t));return e?(0,o.cloneElement)(e,{className:"margin-top--md"}):null}return o.createElement("div",{className:"margin-top--md"},l.map(((e,n)=>(0,o.cloneElement)(e,{key:n,hidden:e.props.value!==t}))))}function w(e){const n=y(e);return o.createElement("div",{className:(0,l.A)("tabs-container",h.tabList)},o.createElement(f,(0,t.A)({},e,n)),o.createElement(v,(0,t.A)({},e,n)))}function N(e){const n=(0,b.A)();return o.createElement(w,(0,t.A)({key:String(n)},e))}},7279:(e,n,a)=>{a.r(n),a.d(n,{assets:()=>u,contentTitle:()=>s,default:()=>g,frontMatter:()=>i,metadata:()=>p,toc:()=>c});var t=a(8102),o=(a(6540),a(5680)),l=a(1253),r=a(6185);const i={title:"Introduction to @ima/cli",description:"CLI > Introduction to @ima/cli"},s=void 0,p={unversionedId:"cli/cli",id:"cli/cli",title:"Introduction to @ima/cli",description:"CLI > Introduction to @ima/cli",source:"@site/../docs/cli/cli.md",sourceDirName:"cli",slug:"/cli/",permalink:"/cli/",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/cli/cli.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Introduction to @ima/cli",description:"CLI > Introduction to @ima/cli"},sidebar:"docs",previous:{title:"Dynamic imports",permalink:"/advanced-features/dynamic-imports"},next:{title:"Compiler features",permalink:"/cli/compiler-features"}},u={},c=[{value:"Development",id:"development",level:2},{value:"Build",id:"build",level:2},{value:"CLI options",id:"cli-options",level:2},{value:"--version",id:"--version",level:3},{value:"--help",id:"--help",level:3},{value:"--clean",id:"--clean",level:3},{value:"--clearCache",id:"--clearcache",level:3},{value:"--verbose",id:"--verbose",level:3},{value:"--inspect",id:"--inspect",level:3},{value:"--ignoreWarnings",id:"--ignorewarnings",level:3},{value:"--open",id:"--open",level:3},{value:"--openUrl",id:"--openurl",level:3},{value:"--legacy",id:"--legacy",level:3},{value:"--forceLegacy",id:"--forcelegacy",level:3},{value:"--forceSPA",id:"--forcespa",level:3},{value:"--profile",id:"--profile",level:3},{value:"--writeToDisk",id:"--writetodisk",level:3},{value:"--reactRefresh",id:"--reactrefresh",level:3},{value:"--lazyServer",id:"--lazyserver",level:3},{value:"Dev server options",id:"dev-server-options",level:2},{value:"--port",id:"--port",level:3},{value:"--hostname",id:"--hostname",level:3},{value:"--publicUrl",id:"--publicurl",level:3}],d={toc:c},m="wrapper";function g(e){let{components:n,...a}=e;return(0,o.yg)(m,(0,t.A)({},d,a,{components:n,mdxType:"MDXLayout"}),(0,o.yg)("p",null,"The ",(0,o.yg)("strong",{parentName:"p"},"IMA.js CLI")," allows you to build and watch your application for changes during development. These features are handle by the only two currently supported commands ",(0,o.yg)("inlineCode",{parentName:"p"},"build")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"dev"),"."),(0,o.yg)("p",null,"You can always list available commands by running:"),(0,o.yg)(l.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,o.yg)(r.A,{value:"npm",mdxType:"TabItem"},(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre",className:"language-bash"},"npx ima --help\n"))),(0,o.yg)(r.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre",className:"language-bash"},"npx ima --help\n"))),(0,o.yg)(r.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre",className:"language-bash"},"npx ima --help\n")))),(0,o.yg)("admonition",{type:"note"},(0,o.yg)("p",{parentName:"admonition"},(0,o.yg)("a",{parentName:"p",href:"https://www.npmjs.com/package/npx"},"npx")," comes pre-installed with npm 5.2+ and higher.")),(0,o.yg)("p",null,"This should produce following output:"),(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre"},"Usage: ima \n\nCommands:\n ima build Build an application for production\n ima dev Run application in development watch mode\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n")),(0,o.yg)("h2",{id:"development"},"Development"),(0,o.yg)("p",null,"The ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev")," command starts the application in the ",(0,o.yg)("strong",{parentName:"p"},"development")," mode with HMR, error-overlay, source maps and other debugging tools enabled."),(0,o.yg)("p",null,"By default the application starts on ",(0,o.yg)("a",{parentName:"p",href:"http://localhost:3001"},"http://localhost:3001")," with ",(0,o.yg)("a",{parentName:"p",href:"./advanced-features#dev-server"},"companion dev server")," running at ",(0,o.yg)("a",{parentName:"p",href:"http://localhost:3101"},"http://localhost:3101"),". These can be further customized through the app ",(0,o.yg)("strong",{parentName:"p"},"environment")," settings and CLI arguments."),(0,o.yg)("p",null,"You can also run ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev --help")," to list all available options that you can use:"),(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre"},"ima dev\n\nRun application in development watch mode\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n --clean Clean build folder before building the application [boolean] [default: true]\n --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]\n --verbose Use default webpack CLI output instead of custom one [boolean]\n --inspect Enable Node inspector mode [boolean]\n --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]\n --open Opens browser window after server has been started [boolean] [default: true]\n --openUrl Custom URL used when opening browser window [string]\n --legacy Runs application in legacy mode [boolean] [default: false]\n --forceLegacy Forces runner.js to execute legacy client code [boolean] [default: false]\n --forceSPA Forces application to run in SPA mode [boolean] [default: false]\n --writeToDisk Write static files to disk, instead of serving it from memory [boolean] [default: false]\n --reactRefresh Enable/disable react fast refresh for React components [boolean] [default: true]\n --lazyServer Enable/disable lazy init of server app factory [boolean] [default: true]\n --port Dev server port (overrides ima.config.js settings) [number]\n --hostname Dev server hostname (overrides ima.config.js settings) [string]\n --publicUrl Dev server publicUrl (overrides ima.config.js settings) [string]\n")),(0,o.yg)("admonition",{type:"info"},(0,o.yg)("p",{parentName:"admonition"},"Any of the above mentioned options can be combined together in all different combinations and all options have specified default value. This means that in normal cases you can run ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev")," without any additional arguments.")),(0,o.yg)("h2",{id:"build"},"Build"),(0,o.yg)("p",null,"Builds the application in production mode with all optimizations enabled (compression, minification, etc.). The ",(0,o.yg)("inlineCode",{parentName:"p"},"build")," command drops some options compared to the ",(0,o.yg)("inlineCode",{parentName:"p"},"dev")," command. While adding few build specific commands. ",(0,o.yg)("inlineCode",{parentName:"p"},"npx build --help")," produces:"),(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre"},"ima build\n\nBuild an application for production\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n --clean Clean build folder before building the application [boolean] [default: true]\n --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]\n --verbose Use default webpack CLI output instead of custom one [boolean]\n --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]\n --profile Turn on profiling support in production [boolean] [default: false]\n")),(0,o.yg)("h2",{id:"cli-options"},"CLI options"),(0,o.yg)("p",null,"Most of the following options are available for both ",(0,o.yg)("inlineCode",{parentName:"p"},"dev")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"build")," commands, however some may be exclusive to only one of them. You can always use the ",(0,o.yg)("inlineCode",{parentName:"p"},"--help")," argument to show all available options for each command."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"When you run into any issues with the application build, you can always run the app with ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev --clearCache")," to make sure that all cache and tmp files are deleted before next build and see if this resolves your issues."),(0,o.yg)("p",{parentName:"admonition"},"Similarly you can use the ",(0,o.yg)("inlineCode",{parentName:"p"},"--verbose")," option to show more information during build that can aid you in ",(0,o.yg)("strong",{parentName:"p"},"debugging process")," in case anything happens.")),(0,o.yg)("h3",{id:"--version"},"--version"),(0,o.yg)("p",null,"Prints ",(0,o.yg)("inlineCode",{parentName:"p"},"@ima/cli")," version."),(0,o.yg)("h3",{id:"--help"},"--help"),(0,o.yg)("p",null,"Prints help dialog."),(0,o.yg)("h3",{id:"--clean"},"--clean"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Deletes ",(0,o.yg)("inlineCode",{parentName:"p"},"./build")," folder before running the application."),(0,o.yg)("h3",{id:"--clearcache"},"--clearCache"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Clears ",(0,o.yg)("inlineCode",{parentName:"p"},"./node_modules/.cache")," folder. This is used to store webpack filesystem cache and other webpack loader and plugins cache."),(0,o.yg)("h3",{id:"--verbose"},"--verbose"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Disables custom CLI logging style in favor of default webpack CLI verbose. This can be useful for debugging."),(0,o.yg)("h3",{id:"--inspect"},"--inspect"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Disable/enable node ",(0,o.yg)("a",{parentName:"p",href:"https://nodejs.org/en/docs/guides/debugging-getting-started"},"inspector")," mode."),(0,o.yg)("h3",{id:"--ignorewarnings"},"--ignoreWarnings"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Ignore reporting of webpack warning messages. The CLI automatically caches all existing warnings and shows just new warnings rebuilds in watch mode."),(0,o.yg)("h3",{id:"--open"},"--open"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Enable/disable auto opening of app URL in the browser window on startup."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"If you find this option annoying, you can completely ",(0,o.yg)("strong",{parentName:"p"},"disable this feature across all IMA.js applications")," by putting ",(0,o.yg)("inlineCode",{parentName:"p"},"IMA_CLI_OPEN=false")," in your environment.")),(0,o.yg)("h3",{id:"--openurl"},"--openUrl"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Allows you to customize URL which is opened when the server starts in development mode."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"You can also use ",(0,o.yg)("inlineCode",{parentName:"p"},"IMA_CLI_OPEN_URL='http://ima.dev:3001'")," env variable to set this option."),(0,o.yg)("p",{parentName:"admonition"},"This is usefull when you have project-specific URLs. You can then set this environment variable in application's ",(0,o.yg)("inlineCode",{parentName:"p"},"ima.config.js")," and don't have to worry about using ",(0,o.yg)("inlineCode",{parentName:"p"},"--openUrl")," CLI argument everytime you're starting the application in dev mode.")),(0,o.yg)("h3",{id:"--legacy"},"--legacy"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"By default the CLI only builds ",(0,o.yg)("inlineCode",{parentName:"p"},"es")," version of JS files in development mode. Use this option to enable ",(0,o.yg)("a",{parentName:"p",href:"./compiler-features#server-and-client-bundles"},"additional build of non es version"),"."),(0,o.yg)("h3",{id:"--forcelegacy"},"--forceLegacy"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Enables ",(0,o.yg)("inlineCode",{parentName:"p"},"legacy")," mode and forces runner.js to load legacy code even if targeted browser supports the latest client es version."),(0,o.yg)("h3",{id:"--forcespa"},"--forceSPA"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Forces the application to run in SPA mode."),(0,o.yg)("h3",{id:"--profile"},"--profile"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Disables some optimizations to allow for better debugging while also trying to be as close to the production build as possible. Currently this option disables mangling of classes and functions, which produces more readable stack traces."),(0,o.yg)("h3",{id:"--writetodisk"},"--writeToDisk"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"By default the app ",(0,o.yg)("strong",{parentName:"p"},"client static files are served from memory")," in dev mode. Using this option you can force webpack to write these files and serve them from the disk."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"This option can be useful in some cases where you need to take a look at the compile source code, where it's easier to browse these files locally, rather than on the static server.")),(0,o.yg)("h3",{id:"--reactrefresh"},"--reactRefresh"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Disable/enable ",(0,o.yg)("a",{parentName:"p",href:"https://github.com/pmmmwh/react-refresh-webpack-plugin"},"react fast refresh")," for React components."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"Disable this option if you are watching and editing ",(0,o.yg)("inlineCode",{parentName:"p"},"node_modules")," files, this may result in less performant but more stable HMR experience.")),(0,o.yg)("h3",{id:"--lazyserver"},"--lazyServer"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Disable/enable lazy init of server app factory."),(0,o.yg)("h2",{id:"dev-server-options"},"Dev server options"),(0,o.yg)("p",null,"Following options are used to customize the companion dev server location (only for ",(0,o.yg)("inlineCode",{parentName:"p"},"dev")," command). These can be useful if you have some special dev environment, where you have an issue with the default configuration."),(0,o.yg)("admonition",{type:"note"},(0,o.yg)("p",{parentName:"admonition"},"If you provide ",(0,o.yg)("inlineCode",{parentName:"p"},"port")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"hostname"),", you don't need to define the ",(0,o.yg)("inlineCode",{parentName:"p"},"publicUrl"),", the CLI will create it automatically, unless the ",(0,o.yg)("inlineCode",{parentName:"p"},"publicUrl")," is completely different than the ",(0,o.yg)("inlineCode",{parentName:"p"},"hostname")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"port")," provided.")),(0,o.yg)("h3",{id:"--port"},"--port"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"number"))),(0,o.yg)("p",null,"Dev server port."),(0,o.yg)("h3",{id:"--hostname"},"--hostname"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"string"))),(0,o.yg)("p",null,"Dev server hostname, for example: ",(0,o.yg)("inlineCode",{parentName:"p"},"localhost"),", or ",(0,o.yg)("inlineCode",{parentName:"p"},"127.0.0.1"),"."),(0,o.yg)("h3",{id:"--publicurl"},"--publicUrl"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"string"))),(0,o.yg)("p",null,"Dev server public url, for example: ",(0,o.yg)("inlineCode",{parentName:"p"},"http://localhost:3101"),"."))}g.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[6098],{5680:(e,n,a)=>{a.d(n,{xA:()=>u,yg:()=>g});var t=a(6540);function o(e,n,a){return n in e?Object.defineProperty(e,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[n]=a,e}function l(e,n){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);n&&(t=t.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),a.push.apply(a,t)}return a}function r(e){for(var n=1;n=0||(o[a]=e[a]);return o}(e,n);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(o[a]=e[a])}return o}var s=t.createContext({}),p=function(e){var n=t.useContext(s),a=n;return e&&(a="function"==typeof e?e(n):r(r({},n),e)),a},u=function(e){var n=p(e.components);return t.createElement(s.Provider,{value:n},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return t.createElement(t.Fragment,{},n)}},m=t.forwardRef((function(e,n){var a=e.components,o=e.mdxType,l=e.originalType,s=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),c=p(a),m=o,g=c["".concat(s,".").concat(m)]||c[m]||d[m]||l;return a?t.createElement(g,r(r({ref:n},u),{},{components:a})):t.createElement(g,r({ref:n},u))}));function g(e,n){var a=arguments,o=n&&n.mdxType;if("string"==typeof e||o){var l=a.length,r=new Array(l);r[0]=m;var i={};for(var s in n)hasOwnProperty.call(n,s)&&(i[s]=n[s]);i.originalType=e,i[c]="string"==typeof e?e:o,r[1]=i;for(var p=2;p{a.d(n,{A:()=>r});var t=a(6540),o=a(8017);const l={tabItem:"tabItem_Ymn6"};function r(e){let{children:n,hidden:a,className:r}=e;return t.createElement("div",{role:"tabpanel",className:(0,o.A)(l.tabItem,r),hidden:a},n)}},1253:(e,n,a)=>{a.d(n,{A:()=>N});var t=a(8102),o=a(6540),l=a(8017),r=a(3104),i=a(9519),s=a(7485),p=a(1682),u=a(9466);function c(e){return function(e){return o.Children.map(e,(e=>{if(!e||(0,o.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:n,label:a,attributes:t,default:o}}=e;return{value:n,label:a,attributes:t,default:o}}))}function d(e){const{values:n,children:a}=e;return(0,o.useMemo)((()=>{const e=n??c(a);return function(e){const n=(0,p.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,a])}function m(e){let{value:n,tabValues:a}=e;return a.some((e=>e.value===n))}function g(e){let{queryString:n=!1,groupId:a}=e;const t=(0,i.W6)(),l=function(e){let{queryString:n=!1,groupId:a}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:n,groupId:a});return[(0,s.aZ)(l),(0,o.useCallback)((e=>{if(!l)return;const n=new URLSearchParams(t.location.search);n.set(l,e),t.replace({...t.location,search:n.toString()})}),[l,t])]}function y(e){const{defaultValue:n,queryString:a=!1,groupId:t}=e,l=d(e),[r,i]=(0,o.useState)((()=>function(e){let{defaultValue:n,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=a.find((e=>e.default))??a[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:l}))),[s,p]=g({queryString:a,groupId:t}),[c,y]=function(e){let{groupId:n}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,l]=(0,u.Dv)(a);return[t,(0,o.useCallback)((e=>{a&&l.set(e)}),[a,l])]}({groupId:t}),b=(()=>{const e=s??c;return m({value:e,tabValues:l})?e:null})();(0,o.useLayoutEffect)((()=>{b&&i(b)}),[b]);return{selectedValue:r,selectValue:(0,o.useCallback)((e=>{if(!m({value:e,tabValues:l}))throw new Error(`Can't select invalid tab value=${e}`);i(e),p(e),y(e)}),[p,y,l]),tabValues:l}}var b=a(2303);const h={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function f(e){let{className:n,block:a,selectedValue:i,selectValue:s,tabValues:p}=e;const u=[],{blockElementScrollPositionUntilNextRender:c}=(0,r.a_)(),d=e=>{const n=e.currentTarget,a=u.indexOf(n),t=p[a].value;t!==i&&(c(n),s(t))},m=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=u.indexOf(e.currentTarget)+1;n=u[a]??u[0];break}case"ArrowLeft":{const a=u.indexOf(e.currentTarget)-1;n=u[a]??u[u.length-1];break}}n?.focus()};return o.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,l.A)("tabs",{"tabs--block":a},n)},p.map((e=>{let{value:n,label:a,attributes:r}=e;return o.createElement("li",(0,t.A)({role:"tab",tabIndex:i===n?0:-1,"aria-selected":i===n,key:n,ref:e=>u.push(e),onKeyDown:m,onClick:d},r,{className:(0,l.A)("tabs__item",h.tabItem,r?.className,{"tabs__item--active":i===n})}),a??n)})))}function v(e){let{lazy:n,children:a,selectedValue:t}=e;const l=(Array.isArray(a)?a:[a]).filter(Boolean);if(n){const e=l.find((e=>e.props.value===t));return e?(0,o.cloneElement)(e,{className:"margin-top--md"}):null}return o.createElement("div",{className:"margin-top--md"},l.map(((e,n)=>(0,o.cloneElement)(e,{key:n,hidden:e.props.value!==t}))))}function w(e){const n=y(e);return o.createElement("div",{className:(0,l.A)("tabs-container",h.tabList)},o.createElement(f,(0,t.A)({},e,n)),o.createElement(v,(0,t.A)({},e,n)))}function N(e){const n=(0,b.A)();return o.createElement(w,(0,t.A)({key:String(n)},e))}},7279:(e,n,a)=>{a.r(n),a.d(n,{assets:()=>u,contentTitle:()=>s,default:()=>g,frontMatter:()=>i,metadata:()=>p,toc:()=>c});var t=a(8102),o=(a(6540),a(5680)),l=a(1253),r=a(6185);const i={title:"Introduction to @ima/cli",description:"CLI > Introduction to @ima/cli"},s=void 0,p={unversionedId:"cli/cli",id:"cli/cli",title:"Introduction to @ima/cli",description:"CLI > Introduction to @ima/cli",source:"@site/../docs/cli/cli.md",sourceDirName:"cli",slug:"/cli/",permalink:"/cli/",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/cli/cli.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Introduction to @ima/cli",description:"CLI > Introduction to @ima/cli"},sidebar:"docs",previous:{title:"Dynamic imports",permalink:"/advanced-features/dynamic-imports"},next:{title:"Compiler features",permalink:"/cli/compiler-features"}},u={},c=[{value:"Development",id:"development",level:2},{value:"Build",id:"build",level:2},{value:"CLI options",id:"cli-options",level:2},{value:"--version",id:"--version",level:3},{value:"--help",id:"--help",level:3},{value:"--clean",id:"--clean",level:3},{value:"--clearCache",id:"--clearcache",level:3},{value:"--verbose",id:"--verbose",level:3},{value:"--inspect",id:"--inspect",level:3},{value:"--ignoreWarnings",id:"--ignorewarnings",level:3},{value:"--open",id:"--open",level:3},{value:"--openUrl",id:"--openurl",level:3},{value:"--legacy",id:"--legacy",level:3},{value:"--forceLegacy",id:"--forcelegacy",level:3},{value:"--forceSPA",id:"--forcespa",level:3},{value:"--profile",id:"--profile",level:3},{value:"--writeToDisk",id:"--writetodisk",level:3},{value:"--reactRefresh",id:"--reactrefresh",level:3},{value:"--lazyServer",id:"--lazyserver",level:3},{value:"Dev server options",id:"dev-server-options",level:2},{value:"--port",id:"--port",level:3},{value:"--hostname",id:"--hostname",level:3},{value:"--publicUrl",id:"--publicurl",level:3}],d={toc:c},m="wrapper";function g(e){let{components:n,...a}=e;return(0,o.yg)(m,(0,t.A)({},d,a,{components:n,mdxType:"MDXLayout"}),(0,o.yg)("p",null,"The ",(0,o.yg)("strong",{parentName:"p"},"IMA.js CLI")," allows you to build and watch your application for changes during development. These features are handle by the only two currently supported commands ",(0,o.yg)("inlineCode",{parentName:"p"},"build")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"dev"),"."),(0,o.yg)("p",null,"You can always list available commands by running:"),(0,o.yg)(l.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,o.yg)(r.A,{value:"npm",mdxType:"TabItem"},(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre",className:"language-bash"},"npx ima --help\n"))),(0,o.yg)(r.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre",className:"language-bash"},"npx ima --help\n"))),(0,o.yg)(r.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre",className:"language-bash"},"npx ima --help\n")))),(0,o.yg)("admonition",{type:"note"},(0,o.yg)("p",{parentName:"admonition"},(0,o.yg)("a",{parentName:"p",href:"https://www.npmjs.com/package/npx"},"npx")," comes pre-installed with npm 5.2+ and higher.")),(0,o.yg)("p",null,"This should produce following output:"),(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre"},"Usage: ima \n\nCommands:\n ima build Build an application for production\n ima dev Run application in development watch mode\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n")),(0,o.yg)("h2",{id:"development"},"Development"),(0,o.yg)("p",null,"The ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev")," command starts the application in the ",(0,o.yg)("strong",{parentName:"p"},"development")," mode with HMR, error-overlay, source maps and other debugging tools enabled."),(0,o.yg)("p",null,"By default the application starts on ",(0,o.yg)("a",{parentName:"p",href:"http://localhost:3001"},"http://localhost:3001")," with ",(0,o.yg)("a",{parentName:"p",href:"./advanced-features#dev-server"},"companion dev server")," running at ",(0,o.yg)("a",{parentName:"p",href:"http://localhost:3101"},"http://localhost:3101"),". These can be further customized through the app ",(0,o.yg)("strong",{parentName:"p"},"environment")," settings and CLI arguments."),(0,o.yg)("p",null,"You can also run ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev --help")," to list all available options that you can use:"),(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre"},"ima dev\n\nRun application in development watch mode\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n --clean Clean build folder before building the application [boolean] [default: true]\n --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]\n --verbose Use default webpack CLI output instead of custom one [boolean]\n --inspect Enable Node inspector mode [boolean]\n --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]\n --open Opens browser window after server has been started [boolean] [default: true]\n --openUrl Custom URL used when opening browser window [string]\n --legacy Runs application in legacy mode [boolean] [default: false]\n --forceLegacy Forces runner.js to execute legacy client code [boolean] [default: false]\n --forceSPA Forces application to run in SPA mode [boolean] [default: false]\n --writeToDisk Write static files to disk, instead of serving it from memory [boolean] [default: false]\n --reactRefresh Enable/disable react fast refresh for React components [boolean] [default: true]\n --lazyServer Enable/disable lazy init of server app factory [boolean] [default: true]\n --port Dev server port (overrides ima.config.js settings) [number]\n --hostname Dev server hostname (overrides ima.config.js settings) [string]\n --publicUrl Dev server publicUrl (overrides ima.config.js settings) [string]\n")),(0,o.yg)("admonition",{type:"info"},(0,o.yg)("p",{parentName:"admonition"},"Any of the above mentioned options can be combined together in all different combinations and all options have specified default value. This means that in normal cases you can run ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev")," without any additional arguments.")),(0,o.yg)("h2",{id:"build"},"Build"),(0,o.yg)("p",null,"Builds the application in production mode with all optimizations enabled (compression, minification, etc.). The ",(0,o.yg)("inlineCode",{parentName:"p"},"build")," command drops some options compared to the ",(0,o.yg)("inlineCode",{parentName:"p"},"dev")," command. While adding few build specific commands. ",(0,o.yg)("inlineCode",{parentName:"p"},"npx build --help")," produces:"),(0,o.yg)("pre",null,(0,o.yg)("code",{parentName:"pre"},"ima build\n\nBuild an application for production\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n --clean Clean build folder before building the application [boolean] [default: true]\n --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]\n --verbose Use default webpack CLI output instead of custom one [boolean]\n --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]\n --profile Turn on profiling support in production [boolean] [default: false]\n")),(0,o.yg)("h2",{id:"cli-options"},"CLI options"),(0,o.yg)("p",null,"Most of the following options are available for both ",(0,o.yg)("inlineCode",{parentName:"p"},"dev")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"build")," commands, however some may be exclusive to only one of them. You can always use the ",(0,o.yg)("inlineCode",{parentName:"p"},"--help")," argument to show all available options for each command."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"When you run into any issues with the application build, you can always run the app with ",(0,o.yg)("inlineCode",{parentName:"p"},"npx ima dev --clearCache")," to make sure that all cache and tmp files are deleted before next build and see if this resolves your issues."),(0,o.yg)("p",{parentName:"admonition"},"Similarly you can use the ",(0,o.yg)("inlineCode",{parentName:"p"},"--verbose")," option to show more information during build that can aid you in ",(0,o.yg)("strong",{parentName:"p"},"debugging process")," in case anything happens.")),(0,o.yg)("h3",{id:"--version"},"--version"),(0,o.yg)("p",null,"Prints ",(0,o.yg)("inlineCode",{parentName:"p"},"@ima/cli")," version."),(0,o.yg)("h3",{id:"--help"},"--help"),(0,o.yg)("p",null,"Prints help dialog."),(0,o.yg)("h3",{id:"--clean"},"--clean"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Deletes ",(0,o.yg)("inlineCode",{parentName:"p"},"./build")," folder before running the application."),(0,o.yg)("h3",{id:"--clearcache"},"--clearCache"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Clears ",(0,o.yg)("inlineCode",{parentName:"p"},"./node_modules/.cache")," folder. This is used to store webpack filesystem cache and other webpack loader and plugins cache."),(0,o.yg)("h3",{id:"--verbose"},"--verbose"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Disables custom CLI logging style in favor of default webpack CLI verbose. This can be useful for debugging."),(0,o.yg)("h3",{id:"--inspect"},"--inspect"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Disable/enable node ",(0,o.yg)("a",{parentName:"p",href:"https://nodejs.org/en/docs/guides/debugging-getting-started"},"inspector")," mode."),(0,o.yg)("h3",{id:"--ignorewarnings"},"--ignoreWarnings"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Ignore reporting of webpack warning messages. The CLI automatically caches all existing warnings and shows just new warnings rebuilds in watch mode."),(0,o.yg)("h3",{id:"--open"},"--open"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Enable/disable auto opening of app URL in the browser window on startup."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"If you find this option annoying, you can completely ",(0,o.yg)("strong",{parentName:"p"},"disable this feature across all IMA.js applications")," by putting ",(0,o.yg)("inlineCode",{parentName:"p"},"IMA_CLI_OPEN=false")," in your environment.")),(0,o.yg)("h3",{id:"--openurl"},"--openUrl"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Allows you to customize URL which is opened when the server starts in development mode."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"You can also use ",(0,o.yg)("inlineCode",{parentName:"p"},"IMA_CLI_OPEN_URL='http://ima.dev:3001'")," env variable to set this option."),(0,o.yg)("p",{parentName:"admonition"},"This is usefull when you have project-specific URLs. You can then set this environment variable in application's ",(0,o.yg)("inlineCode",{parentName:"p"},"ima.config.js")," and don't have to worry about using ",(0,o.yg)("inlineCode",{parentName:"p"},"--openUrl")," CLI argument everytime you're starting the application in dev mode.")),(0,o.yg)("h3",{id:"--legacy"},"--legacy"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"By default the CLI only builds ",(0,o.yg)("inlineCode",{parentName:"p"},"es")," version of JS files in development mode. Use this option to enable ",(0,o.yg)("a",{parentName:"p",href:"./compiler-features#server-and-client-bundles"},"additional build of non es version"),"."),(0,o.yg)("h3",{id:"--forcelegacy"},"--forceLegacy"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Enables ",(0,o.yg)("inlineCode",{parentName:"p"},"legacy")," mode and forces runner.js to load legacy code even if targeted browser supports the latest client es version."),(0,o.yg)("h3",{id:"--forcespa"},"--forceSPA"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Forces the application to run in SPA mode."),(0,o.yg)("h3",{id:"--profile"},"--profile"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"Disables some optimizations to allow for better debugging while also trying to be as close to the production build as possible. Currently this option disables mangling of classes and functions, which produces more readable stack traces."),(0,o.yg)("h3",{id:"--writetodisk"},"--writeToDisk"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = false"))),(0,o.yg)("p",null,"By default the app ",(0,o.yg)("strong",{parentName:"p"},"client static files are served from memory")," in dev mode. Using this option you can force webpack to write these files and serve them from the disk."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"This option can be useful in some cases where you need to take a look at the compile source code, where it's easier to browse these files locally, rather than on the static server.")),(0,o.yg)("h3",{id:"--reactrefresh"},"--reactRefresh"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Disable/enable ",(0,o.yg)("a",{parentName:"p",href:"https://github.com/pmmmwh/react-refresh-webpack-plugin"},"react fast refresh")," for React components."),(0,o.yg)("admonition",{type:"tip"},(0,o.yg)("p",{parentName:"admonition"},"Disable this option if you are watching and editing ",(0,o.yg)("inlineCode",{parentName:"p"},"node_modules")," files, this may result in less performant but more stable HMR experience.")),(0,o.yg)("h3",{id:"--lazyserver"},"--lazyServer"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,o.yg)("p",null,"Disable/enable lazy init of server app factory."),(0,o.yg)("h2",{id:"dev-server-options"},"Dev server options"),(0,o.yg)("p",null,"Following options are used to customize the companion dev server location (only for ",(0,o.yg)("inlineCode",{parentName:"p"},"dev")," command). These can be useful if you have some special dev environment, where you have an issue with the default configuration."),(0,o.yg)("admonition",{type:"note"},(0,o.yg)("p",{parentName:"admonition"},"If you provide ",(0,o.yg)("inlineCode",{parentName:"p"},"port")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"hostname"),", you don't need to define the ",(0,o.yg)("inlineCode",{parentName:"p"},"publicUrl"),", the CLI will create it automatically, unless the ",(0,o.yg)("inlineCode",{parentName:"p"},"publicUrl")," is completely different than the ",(0,o.yg)("inlineCode",{parentName:"p"},"hostname")," and ",(0,o.yg)("inlineCode",{parentName:"p"},"port")," provided.")),(0,o.yg)("h3",{id:"--port"},"--port"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"number"))),(0,o.yg)("p",null,"Dev server port."),(0,o.yg)("h3",{id:"--hostname"},"--hostname"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"string"))),(0,o.yg)("p",null,"Dev server hostname, for example: ",(0,o.yg)("inlineCode",{parentName:"p"},"localhost"),", or ",(0,o.yg)("inlineCode",{parentName:"p"},"127.0.0.1"),"."),(0,o.yg)("h3",{id:"--publicurl"},"--publicUrl"),(0,o.yg)("blockquote",null,(0,o.yg)("p",{parentName:"blockquote"},(0,o.yg)("inlineCode",{parentName:"p"},"string"))),(0,o.yg)("p",null,"Dev server public url, for example: ",(0,o.yg)("inlineCode",{parentName:"p"},"http://localhost:3101"),"."))}g.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/25aeb21c.3e24f869.js b/assets/js/25aeb21c.9659c894.js similarity index 99% rename from assets/js/25aeb21c.3e24f869.js rename to assets/js/25aeb21c.9659c894.js index 7b33505bc..fc6ab632e 100644 --- a/assets/js/25aeb21c.3e24f869.js +++ b/assets/js/25aeb21c.9659c894.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[7894],{5050:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/diagram-router-683726941d228a587bbf7ffda06d3c45.png"},5680:(e,n,t)=>{t.d(n,{xA:()=>u,yg:()=>m});var r=t(6540);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var s=r.createContext({}),p=function(e){var n=r.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},u=function(e){var n=p(e.components);return r.createElement(s.Provider,{value:n},e.children)},d="mdxType",c={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},g=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),d=p(t),g=a,m=d["".concat(s,".").concat(g)]||d[g]||c[g]||o;return t?r.createElement(m,i(i({ref:n},u),{},{components:t})):r.createElement(m,i({ref:n},u))}));function m(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var o=t.length,i=new Array(o);i[0]=g;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[d]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=t(8102),a=(t(6540),t(5680));const o={title:"Introduction",description:"Basic features > Routing >\xa0Introduction"},i=void 0,l={unversionedId:"basic-features/routing/introduction",id:"basic-features/routing/introduction",title:"Introduction",description:"Basic features > Routing >\xa0Introduction",source:"@site/../docs/basic-features/routing/introduction.md",sourceDirName:"basic-features/routing",slug:"/basic-features/routing/introduction",permalink:"/basic-features/routing/introduction",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/routing/introduction.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Introduction",description:"Basic features > Routing >\xa0Introduction"},sidebar:"docs",previous:{title:"Data fetching",permalink:"/basic-features/data-fetching"},next:{title:"Dynamic Routes",permalink:"/basic-features/routing/dynamic-routes"}},s={},p=[{value:"Setting up Router",id:"setting-up-router",level:2},{value:"name",id:"name",level:3},{value:"pathExpression",id:"pathexpression",level:3},{value:"controller",id:"controller",level:3},{value:"view",id:"view",level:3},{value:"options",id:"options",level:3},{value:"onlyUpdate",id:"onlyupdate",level:4},{value:"autoScroll",id:"autoscroll",level:4},{value:"allowSPA",id:"allowspa",level:4},{value:"documentView",id:"documentview",level:4},{value:"managedRootView",id:"managedrootview",level:4},{value:"viewAdapter",id:"viewadapter",level:4},{value:"middlewares",id:"middlewares",level:4},{value:"Route params substitutions",id:"route-params-substitutions",level:2},{value:"Optional parameters",id:"optional-parameters",level:3},{value:"Linking between routes",id:"linking-between-routes",level:2},{value:"Generating links outside of app components",id:"generating-links-outside-of-app-components",level:3},{value:"Error and NotFound route names",id:"error-and-notfound-route-names",level:2},{value:"Redirects",id:"redirects",level:2},{value:"Method signature",id:"method-signature",level:3},{value:"url",id:"url",level:3},{value:"options",id:"options-1",level:3},{value:"httpStatus",id:"httpstatus",level:4},{value:"headers",id:"headers",level:4}],u={toc:p},d="wrapper";function c(e){let{components:n,...o}=e;return(0,a.yg)(d,(0,r.A)({},u,o,{components:n,mdxType:"MDXLayout"}),(0,a.yg)("p",null,"Routing is an essential part of every application that displays multiple pages. It allows to develop each part of an application separately and add new parts instantly. As it happens to be in MVC frameworks, each route targets specific controller which takes control over what happens next after a route is matched."),(0,a.yg)("p",null,(0,a.yg)("img",{src:t(5050).A,width:"881",height:"421"})),(0,a.yg)("h2",{id:"setting-up-router"},"Setting up Router"),(0,a.yg)("p",null,"All routes in IMA.js are registered inside the ",(0,a.yg)("inlineCode",{parentName:"p"},"init")," function in ",(0,a.yg)("inlineCode",{parentName:"p"},"app/config/routes.js"),". Same ",(0,a.yg)("inlineCode",{parentName:"p"},"init")," function can be found in ",(0,a.yg)("inlineCode",{parentName:"p"},"app/config/bind.js"),". See ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container")," documentation for more information about the ",(0,a.yg)("inlineCode",{parentName:"p"},"oc.get()")," function."),(0,a.yg)("p",null,"Usually you should be oke with simple string defined ",(0,a.yg)("a",{parentName:"p",href:"../../api/classes/ima_core.StaticRoute.md"},"StaticRoutes")," (the ones defined below), but the router also has support for more advanced and powerful ",(0,a.yg)("a",{parentName:"p",href:"../../api/classes/ima_core.DynamicRoute.md"},"DynamicRoutes"),". For more information about these see the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/dynamic-routes"},"next section"),"."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nimport HomeController from 'app/page/home/HomeController';\nimport HomeView from 'app/page/home/HomeView';\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add('home', '/', HomeController, HomeView)\n .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)\n .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);\n}\n")),(0,a.yg)("p",null,"The router ",(0,a.yg)("inlineCode",{parentName:"p"},"add")," method has following signature:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"add(name, pathExpression, controller, view, options = undefined);\n")),(0,a.yg)("h3",{id:"name"},"name"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},"\xa0",(0,a.yg)("inlineCode",{parentName:"p"},"string"))),(0,a.yg)("p",null,"This argument represents ",(0,a.yg)("strong",{parentName:"p"},"unique route name"),". You can use this name when ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#linking-between-routes"},"linking between routes")," or getting the ",(0,a.yg)("inlineCode",{parentName:"p"},"route")," instance using ",(0,a.yg)("inlineCode",{parentName:"p"},"getRouteHandler()")," method."),(0,a.yg)("h3",{id:"pathexpression"},"pathExpression"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string |\xa0object"))),(0,a.yg)("p",null,"This can be either ",(0,a.yg)("inlineCode",{parentName:"p"},"object")," for ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/dynamic-routes"},"dynamic routes")," or ",(0,a.yg)("inlineCode",{parentName:"p"},"string")," representing route path. The pathExpression supports **",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#route-params-substitutions"},"parameter substitutions")),(0,a.yg)("h3",{id:"controller"},"controller"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string | function"))),(0,a.yg)("p",null,"Route assigned ",(0,a.yg)("strong",{parentName:"p"},(0,a.yg)("a",{parentName:"strong",href:"/basic-features/controller-lifecycle"},"Controller"))," class (can be a string alias, referring to the controller registered in the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container"),"). It goes through its full ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/controller-lifecycle"},"lifecycle")," and renders the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/views-and-components"},"View"),"."),(0,a.yg)("h3",{id:"view"},"view"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string | function"))),(0,a.yg)("p",null,"Route assigned ",(0,a.yg)("strong",{parentName:"p"},(0,a.yg)("a",{parentName:"strong",href:"/basic-features/views-and-components"},"View"))," class (also can be a string alias, referring to the view registered in the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container"),"). Rendered by the route controller."),(0,a.yg)("h3",{id:"options"},"options"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"object = undefined"))),(0,a.yg)("p",null,"These are optional, however it accepts object with following properties and their respective defaults:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"{\n onlyUpdate: false,\n autoScroll: true,\n allowSPA: true,\n documentView: null,\n managedRootView: null,\n viewAdapter: null,\n middlewares: []\n}\n")),(0,a.yg)("h4",{id:"onlyupdate"},"onlyUpdate"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"boolean | function = false"))),(0,a.yg)("p",null,"When only the parameters of the current route change an ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/controller-lifecycle#update-client"},(0,a.yg)("inlineCode",{parentName:"a"},"update")," method")," of the active controller will be invoked instead of re-instantiating the controller and view. The ",(0,a.yg)("inlineCode",{parentName:"p"},"update")," method receives ",(0,a.yg)("inlineCode",{parentName:"p"},"prevParams")," object containing - as the name suggests - previous route parameters."),(0,a.yg)("p",null,"If you provide function to the ",(0,a.yg)("inlineCode",{parentName:"p"},"onlyUpdate")," option; it receives 2 arguments (instances of previous ",(0,a.yg)("strong",{parentName:"p"},"controller")," and ",(0,a.yg)("strong",{parentName:"p"},"view"),") and it should return ",(0,a.yg)("strong",{parentName:"p"},"boolean"),"."),(0,a.yg)("h4",{id:"autoscroll"},"autoScroll"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,a.yg)("p",null,"Determines whether the page should be ",(0,a.yg)("strong",{parentName:"p"},"scrolled to the top")," when the navigation occurs."),(0,a.yg)("h4",{id:"allowspa"},"allowSPA"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,a.yg)("p",null,"Can be used to make the route to be always served from the server and never using the SPA (when disabled) even if the server is overloaded."),(0,a.yg)("p",null,"This is useful for routes that use different document views (specified by the ",(0,a.yg)("inlineCode",{parentName:"p"},"documentView")," option), for example for rendering the content of iframes."),(0,a.yg)("h4",{id:"documentview"},"documentView"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"AbstractDocumentView = null"))),(0,a.yg)("p",null,"Custom ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#documentview"},"DocumentView"),", should extend the ",(0,a.yg)("inlineCode",{parentName:"p"},"AbstractDocumentView")," from ",(0,a.yg)("inlineCode",{parentName:"p"},"@ima/core"),"."),(0,a.yg)("h4",{id:"managedrootview"},"managedRootView"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"function = null"))),(0,a.yg)("p",null,"Custom ",(0,a.yg)("inlineCode",{parentName:"p"},"ManagedRootView")," component, for more information see ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#managedrootview"},"rendering process"),"."),(0,a.yg)("h4",{id:"viewadapter"},"viewAdapter"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"function = null"))),(0,a.yg)("p",null,"Custom ",(0,a.yg)("inlineCode",{parentName:"p"},"ViewAdapter")," component, for more information see ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#viewadapter"},"rendering process"),"."),(0,a.yg)("h4",{id:"middlewares"},"middlewares"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"function[] = []"))),(0,a.yg)("p",null,"Array of route-specific middlewares. See the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/middlewares"},"middlewares")," section for more information."),(0,a.yg)("h2",{id:"route-params-substitutions"},"Route params substitutions"),(0,a.yg)("p",null,"The parameter name can contain only letters ",(0,a.yg)("inlineCode",{parentName:"p"},"a-zA-Z"),", numbers ",(0,a.yg)("inlineCode",{parentName:"p"},"0-9"),", underscores ",(0,a.yg)("inlineCode",{parentName:"p"},"_")," and hyphens ",(0,a.yg)("inlineCode",{parentName:"p"},"-")," and is preceded by colon ",(0,a.yg)("inlineCode",{parentName:"p"},":"),"."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"router.add(\n 'order-detail',\n // highlight-next-line\n '/user/:userId/orders/:orderId',\n OrderController,\n OrderView\n);\n")),(0,a.yg)("p",null,"The ",(0,a.yg)("inlineCode",{parentName:"p"},"userId")," and ",(0,a.yg)("inlineCode",{parentName:"p"},"orderId")," parameters are then accessible in ",(0,a.yg)("inlineCode",{parentName:"p"},"OrderController")," via ",(0,a.yg)("inlineCode",{parentName:"p"},"this.params"),":"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractController } from '@ima/core';\n\nclass OrderController extends AbstractController {\n load() {\n // highlight-next-line\n const userPromise = this._userService.get(this.params.userId);\n // highlight-next-line\n const orderPromise = this._orderService.get(this.params.orderId);\n\n return {\n user: userPromise,\n order: orderPromise\n }\n }\n}\n")),(0,a.yg)("h3",{id:"optional-parameters"},"Optional parameters"),(0,a.yg)("p",null,"Parameters can also be marked as ",(0,a.yg)("strong",{parentName:"p"},"optional")," by placing question mark ",(0,a.yg)("inlineCode",{parentName:"p"},"?")," after the colon ",(0,a.yg)("inlineCode",{parentName:"p"},":"),"."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"router.add(\n 'user-detail',\n // highlight-next-line\n '/profile/:?userId',\n UserController,\n UserView\n);\n")),(0,a.yg)("admonition",{type:"caution"},(0,a.yg)("p",{parentName:"admonition"},"Optional parameters can be ",(0,a.yg)("strong",{parentName:"p"},"placed only after the last slash"),". Doing otherwise can cause unexpected behavior.")),(0,a.yg)("h2",{id:"linking-between-routes"},"Linking between routes"),(0,a.yg)("p",null,"URLs to routes can be generated via the ",(0,a.yg)("inlineCode",{parentName:"p"},"Router.link()")," public method. These can be then used in ordinary anchor tags and IMA.js makes sure, ",(0,a.yg)("strong",{parentName:"p"},"to handle the site routing in SPA mode"),", rather than doing redirect/reload of the whole page."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-jsx"},"import { AbstractComponent } from '@ima/react-page-renderer';\n\nclass OrderView extends AbstractComponent {\n render() {\n const { user, order } = this.props;\n\n const orderLink = this.link('order-detail', {\n userId: user.id,\n orderId: order.id\n });\n\n return View order\n }\n}\n")),(0,a.yg)("p",null,"This is done by listening to window ",(0,a.yg)("inlineCode",{parentName:"p"},"popstate")," and ",(0,a.yg)("inlineCode",{parentName:"p"},"click")," events and reacting accordingly (in the ",(0,a.yg)("inlineCode",{parentName:"p"},"listen")," method of ",(0,a.yg)("a",{parentName:"p",href:"https://github.com/seznam/ima/blob/next/packages/core/src/router/ClientRouter.js#L113"},"ClientRouter"),", which is called by IMA.js on client during app init). If the handled URL is not valid registered app route, it is handled normally (e.g you are redirected to the target URL)."),(0,a.yg)("admonition",{type:"tip"},(0,a.yg)("p",{parentName:"admonition"},"You can use ",(0,a.yg)("inlineCode",{parentName:"p"},"this.link")," helper method in IMA.js abstract component or the ",(0,a.yg)("inlineCode",{parentName:"p"},"useLink")," hook from the ",(0,a.yg)("a",{parentName:"p",href:"https://github.com/seznam/IMA.js-plugins/tree/master/packages/react-hooks"},"@ima/react-hooks")," plugin in your components and views to generate router links.")),(0,a.yg)("admonition",{type:"note"},(0,a.yg)("p",{parentName:"admonition"},"Under the hood, ",(0,a.yg)("inlineCode",{parentName:"p"},"this.link()")," is only alias for ",(0,a.yg)("inlineCode",{parentName:"p"},"this.utils.$Router.link"),", where ",(0,a.yg)("inlineCode",{parentName:"p"},"this.utils")," is taken from ",(0,a.yg)("inlineCode",{parentName:"p"},"this.context.$Utils"),"."),(0,a.yg)("p",{parentName:"admonition"},"For more information about ",(0,a.yg)("inlineCode",{parentName:"p"},"this.utils")," and ",(0,a.yg)("inlineCode",{parentName:"p"},"$Utils")," objects, take a look at the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#react-context"},"React Context")," in the documentation.")),(0,a.yg)("h3",{id:"generating-links-outside-of-app-components"},"Generating links outside of app components"),(0,a.yg)("p",null,"Linking in ",(0,a.yg)("strong",{parentName:"p"},"Controllers"),", ",(0,a.yg)("strong",{parentName:"p"},"Extensions"),", ",(0,a.yg)("strong",{parentName:"p"},"Helpers")," and other ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container")," classes requires you to import ",(0,a.yg)("inlineCode",{parentName:"p"},"Router")," using ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container#1-dependency-injection"},"dependency injection"),". To do that you can either use ",(0,a.yg)("inlineCode",{parentName:"p"},"Router")," class in the dependency array, or ",(0,a.yg)("inlineCode",{parentName:"p"},"$Router")," string alias:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractController } from '@ima/core';\n\nexport default class DetailController extends AbstractController {\n static get $dependencies() {\n return ['$Router'];\n }\n\n constructor(router) {\n this._router = router;\n }\n\n load() {\n // ...\n }\n}\n")),(0,a.yg)("p",null,"Then you get ",(0,a.yg)("inlineCode",{parentName:"p"},"Router")," instance as the constructor's first argument, which gives you access to it's ",(0,a.yg)("inlineCode",{parentName:"p"},"link")," public method (and many others), that you can use to generate your desired route URL:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"load() {\n const detailLink = this._router.link('order-detail', {\n userId: user.id,\n orderId: order.id\n });\n\n return { detailLink };\n}\n")),(0,a.yg)("h2",{id:"error-and-notfound-route-names"},"Error and NotFound route names"),(0,a.yg)("p",null,"There are two special route names that ",(0,a.yg)("inlineCode",{parentName:"p"},"@ima/core")," exports: ",(0,a.yg)("inlineCode",{parentName:"p"},"RouteNames.ERROR"),", ",(0,a.yg)("inlineCode",{parentName:"p"},"RouteNames.NOT_FOUND"),". You can use these constants to provide custom views and controllers for error handling pages."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nimport { ErrorController, ErrorView } from 'app/page/error';\nimport { NotFoundController, NotFoundView } from 'app/page/not-found';\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add('home', '/', HomeController, HomeView)\n .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)\n .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);\n}\n")),(0,a.yg)("h2",{id:"redirects"},"Redirects"),(0,a.yg)("p",null,"In addition to the ",(0,a.yg)("inlineCode",{parentName:"p"},"link")," method mentioned above (which handles URL generation for given routes), you can use ",(0,a.yg)("inlineCode",{parentName:"p"},"Router.redirect()")," method to ",(0,a.yg)("strong",{parentName:"p"},"redirect directly to the targeted URL"),"."),(0,a.yg)("p",null,"This URL can be either existing app route or external URL. As with links, in this case you also get SPA routing, in case of redirection to different IMA.js app route."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractController, Router } from '@ima/core';\n\nexport default class DetailController extends AbstractController {\n static get $dependencies() {\n return [\n Router // We're using class descriptor in this case for DI\n ];\n }\n\n constructor(router) {\n this._router = router;\n }\n\n init() {\n // highlight-next-line\n this._router.redirect(\n // highlight-next-line\n this._router.link('order-detail', {\n // highlight-next-line\n userId: user.id,\n // highlight-next-line\n orderId: order.id\n // highlight-next-line\n });\n // highlight-next-line\n );\n }\n}\n")),(0,a.yg)("admonition",{type:"info"},(0,a.yg)("p",{parentName:"admonition"},"On client side, redirections are handled by simply changing the ",(0,a.yg)("inlineCode",{parentName:"p"},"window.location.href"),", while on server you're using the express native ",(0,a.yg)("inlineCode",{parentName:"p"},"res.redirect")," method.")),(0,a.yg)("h3",{id:"method-signature"},"Method signature"),(0,a.yg)("p",null,"The redirect method has following signature, while the options object is ",(0,a.yg)("strong",{parentName:"p"},"available only on server side"),":"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"redirect(\n url = '',\n options = {} // Available only on server side\n)\n")),(0,a.yg)("h3",{id:"url"},"url"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string"))),(0,a.yg)("p",null,"Target redirect URL."),(0,a.yg)("h3",{id:"options-1"},"options"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"object = {}"))),(0,a.yg)("p",null,"Additional options, used to customize redirect server response."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"{\n httpStatus: 302,\n headers: undefined,\n}\n")),(0,a.yg)("h4",{id:"httpstatus"},"httpStatus"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"number = 302"))),(0,a.yg)("p",null,"Custom redirect http status code."),(0,a.yg)("h4",{id:"headers"},"headers"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"object = undefined"))),(0,a.yg)("p",null,"Custom response headers."))}c.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[7894],{5050:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/diagram-router-683726941d228a587bbf7ffda06d3c45.png"},5680:(e,n,t)=>{t.d(n,{xA:()=>u,yg:()=>m});var r=t(6540);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var s=r.createContext({}),p=function(e){var n=r.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},u=function(e){var n=p(e.components);return r.createElement(s.Provider,{value:n},e.children)},d="mdxType",c={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},g=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),d=p(t),g=a,m=d["".concat(s,".").concat(g)]||d[g]||c[g]||o;return t?r.createElement(m,i(i({ref:n},u),{},{components:t})):r.createElement(m,i({ref:n},u))}));function m(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var o=t.length,i=new Array(o);i[0]=g;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[d]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=t(8102),a=(t(6540),t(5680));const o={title:"Introduction",description:"Basic features > Routing >\xa0Introduction"},i=void 0,l={unversionedId:"basic-features/routing/introduction",id:"basic-features/routing/introduction",title:"Introduction",description:"Basic features > Routing >\xa0Introduction",source:"@site/../docs/basic-features/routing/introduction.md",sourceDirName:"basic-features/routing",slug:"/basic-features/routing/introduction",permalink:"/basic-features/routing/introduction",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/routing/introduction.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Introduction",description:"Basic features > Routing >\xa0Introduction"},sidebar:"docs",previous:{title:"Data fetching",permalink:"/basic-features/data-fetching"},next:{title:"Dynamic Routes",permalink:"/basic-features/routing/dynamic-routes"}},s={},p=[{value:"Setting up Router",id:"setting-up-router",level:2},{value:"name",id:"name",level:3},{value:"pathExpression",id:"pathexpression",level:3},{value:"controller",id:"controller",level:3},{value:"view",id:"view",level:3},{value:"options",id:"options",level:3},{value:"onlyUpdate",id:"onlyupdate",level:4},{value:"autoScroll",id:"autoscroll",level:4},{value:"allowSPA",id:"allowspa",level:4},{value:"documentView",id:"documentview",level:4},{value:"managedRootView",id:"managedrootview",level:4},{value:"viewAdapter",id:"viewadapter",level:4},{value:"middlewares",id:"middlewares",level:4},{value:"Route params substitutions",id:"route-params-substitutions",level:2},{value:"Optional parameters",id:"optional-parameters",level:3},{value:"Linking between routes",id:"linking-between-routes",level:2},{value:"Generating links outside of app components",id:"generating-links-outside-of-app-components",level:3},{value:"Error and NotFound route names",id:"error-and-notfound-route-names",level:2},{value:"Redirects",id:"redirects",level:2},{value:"Method signature",id:"method-signature",level:3},{value:"url",id:"url",level:3},{value:"options",id:"options-1",level:3},{value:"httpStatus",id:"httpstatus",level:4},{value:"headers",id:"headers",level:4}],u={toc:p},d="wrapper";function c(e){let{components:n,...o}=e;return(0,a.yg)(d,(0,r.A)({},u,o,{components:n,mdxType:"MDXLayout"}),(0,a.yg)("p",null,"Routing is an essential part of every application that displays multiple pages. It allows to develop each part of an application separately and add new parts instantly. As it happens to be in MVC frameworks, each route targets specific controller which takes control over what happens next after a route is matched."),(0,a.yg)("p",null,(0,a.yg)("img",{src:t(5050).A,width:"881",height:"421"})),(0,a.yg)("h2",{id:"setting-up-router"},"Setting up Router"),(0,a.yg)("p",null,"All routes in IMA.js are registered inside the ",(0,a.yg)("inlineCode",{parentName:"p"},"init")," function in ",(0,a.yg)("inlineCode",{parentName:"p"},"app/config/routes.js"),". Same ",(0,a.yg)("inlineCode",{parentName:"p"},"init")," function can be found in ",(0,a.yg)("inlineCode",{parentName:"p"},"app/config/bind.js"),". See ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container")," documentation for more information about the ",(0,a.yg)("inlineCode",{parentName:"p"},"oc.get()")," function."),(0,a.yg)("p",null,"Usually you should be oke with simple string defined ",(0,a.yg)("a",{parentName:"p",href:"../../api/classes/ima_core.StaticRoute.md"},"StaticRoutes")," (the ones defined below), but the router also has support for more advanced and powerful ",(0,a.yg)("a",{parentName:"p",href:"../../api/classes/ima_core.DynamicRoute.md"},"DynamicRoutes"),". For more information about these see the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/dynamic-routes"},"next section"),"."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nimport HomeController from 'app/page/home/HomeController';\nimport HomeView from 'app/page/home/HomeView';\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add('home', '/', HomeController, HomeView)\n .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)\n .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);\n}\n")),(0,a.yg)("p",null,"The router ",(0,a.yg)("inlineCode",{parentName:"p"},"add")," method has following signature:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"add(name, pathExpression, controller, view, options = undefined);\n")),(0,a.yg)("h3",{id:"name"},"name"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},"\xa0",(0,a.yg)("inlineCode",{parentName:"p"},"string"))),(0,a.yg)("p",null,"This argument represents ",(0,a.yg)("strong",{parentName:"p"},"unique route name"),". You can use this name when ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#linking-between-routes"},"linking between routes")," or getting the ",(0,a.yg)("inlineCode",{parentName:"p"},"route")," instance using ",(0,a.yg)("inlineCode",{parentName:"p"},"getRouteHandler()")," method."),(0,a.yg)("h3",{id:"pathexpression"},"pathExpression"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string |\xa0object"))),(0,a.yg)("p",null,"This can be either ",(0,a.yg)("inlineCode",{parentName:"p"},"object")," for ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/dynamic-routes"},"dynamic routes")," or ",(0,a.yg)("inlineCode",{parentName:"p"},"string")," representing route path. The pathExpression supports **",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/introduction#route-params-substitutions"},"parameter substitutions")),(0,a.yg)("h3",{id:"controller"},"controller"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string | function"))),(0,a.yg)("p",null,"Route assigned ",(0,a.yg)("strong",{parentName:"p"},(0,a.yg)("a",{parentName:"strong",href:"/basic-features/controller-lifecycle"},"Controller"))," class (can be a string alias, referring to the controller registered in the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container"),"). It goes through its full ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/controller-lifecycle"},"lifecycle")," and renders the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/views-and-components"},"View"),"."),(0,a.yg)("h3",{id:"view"},"view"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string | function"))),(0,a.yg)("p",null,"Route assigned ",(0,a.yg)("strong",{parentName:"p"},(0,a.yg)("a",{parentName:"strong",href:"/basic-features/views-and-components"},"View"))," class (also can be a string alias, referring to the view registered in the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container"),"). Rendered by the route controller."),(0,a.yg)("h3",{id:"options"},"options"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"object = undefined"))),(0,a.yg)("p",null,"These are optional, however it accepts object with following properties and their respective defaults:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"{\n onlyUpdate: false,\n autoScroll: true,\n allowSPA: true,\n documentView: null,\n managedRootView: null,\n viewAdapter: null,\n middlewares: []\n}\n")),(0,a.yg)("h4",{id:"onlyupdate"},"onlyUpdate"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"boolean | function = false"))),(0,a.yg)("p",null,"When only the parameters of the current route change an ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/controller-lifecycle#update-client"},(0,a.yg)("inlineCode",{parentName:"a"},"update")," method")," of the active controller will be invoked instead of re-instantiating the controller and view. The ",(0,a.yg)("inlineCode",{parentName:"p"},"update")," method receives ",(0,a.yg)("inlineCode",{parentName:"p"},"prevParams")," object containing - as the name suggests - previous route parameters."),(0,a.yg)("p",null,"If you provide function to the ",(0,a.yg)("inlineCode",{parentName:"p"},"onlyUpdate")," option; it receives 2 arguments (instances of previous ",(0,a.yg)("strong",{parentName:"p"},"controller")," and ",(0,a.yg)("strong",{parentName:"p"},"view"),") and it should return ",(0,a.yg)("strong",{parentName:"p"},"boolean"),"."),(0,a.yg)("h4",{id:"autoscroll"},"autoScroll"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,a.yg)("p",null,"Determines whether the page should be ",(0,a.yg)("strong",{parentName:"p"},"scrolled to the top")," when the navigation occurs."),(0,a.yg)("h4",{id:"allowspa"},"allowSPA"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,a.yg)("p",null,"Can be used to make the route to be always served from the server and never using the SPA (when disabled) even if the server is overloaded."),(0,a.yg)("p",null,"This is useful for routes that use different document views (specified by the ",(0,a.yg)("inlineCode",{parentName:"p"},"documentView")," option), for example for rendering the content of iframes."),(0,a.yg)("h4",{id:"documentview"},"documentView"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"AbstractDocumentView = null"))),(0,a.yg)("p",null,"Custom ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#documentview"},"DocumentView"),", should extend the ",(0,a.yg)("inlineCode",{parentName:"p"},"AbstractDocumentView")," from ",(0,a.yg)("inlineCode",{parentName:"p"},"@ima/core"),"."),(0,a.yg)("h4",{id:"managedrootview"},"managedRootView"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"function = null"))),(0,a.yg)("p",null,"Custom ",(0,a.yg)("inlineCode",{parentName:"p"},"ManagedRootView")," component, for more information see ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#managedrootview"},"rendering process"),"."),(0,a.yg)("h4",{id:"viewadapter"},"viewAdapter"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"function = null"))),(0,a.yg)("p",null,"Custom ",(0,a.yg)("inlineCode",{parentName:"p"},"ViewAdapter")," component, for more information see ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#viewadapter"},"rendering process"),"."),(0,a.yg)("h4",{id:"middlewares"},"middlewares"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"function[] = []"))),(0,a.yg)("p",null,"Array of route-specific middlewares. See the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/routing/middlewares"},"middlewares")," section for more information."),(0,a.yg)("h2",{id:"route-params-substitutions"},"Route params substitutions"),(0,a.yg)("p",null,"The parameter name can contain only letters ",(0,a.yg)("inlineCode",{parentName:"p"},"a-zA-Z"),", numbers ",(0,a.yg)("inlineCode",{parentName:"p"},"0-9"),", underscores ",(0,a.yg)("inlineCode",{parentName:"p"},"_")," and hyphens ",(0,a.yg)("inlineCode",{parentName:"p"},"-")," and is preceded by colon ",(0,a.yg)("inlineCode",{parentName:"p"},":"),"."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"router.add(\n 'order-detail',\n // highlight-next-line\n '/user/:userId/orders/:orderId',\n OrderController,\n OrderView\n);\n")),(0,a.yg)("p",null,"The ",(0,a.yg)("inlineCode",{parentName:"p"},"userId")," and ",(0,a.yg)("inlineCode",{parentName:"p"},"orderId")," parameters are then accessible in ",(0,a.yg)("inlineCode",{parentName:"p"},"OrderController")," via ",(0,a.yg)("inlineCode",{parentName:"p"},"this.params"),":"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractController } from '@ima/core';\n\nclass OrderController extends AbstractController {\n load() {\n // highlight-next-line\n const userPromise = this._userService.get(this.params.userId);\n // highlight-next-line\n const orderPromise = this._orderService.get(this.params.orderId);\n\n return {\n user: userPromise,\n order: orderPromise\n }\n }\n}\n")),(0,a.yg)("h3",{id:"optional-parameters"},"Optional parameters"),(0,a.yg)("p",null,"Parameters can also be marked as ",(0,a.yg)("strong",{parentName:"p"},"optional")," by placing question mark ",(0,a.yg)("inlineCode",{parentName:"p"},"?")," after the colon ",(0,a.yg)("inlineCode",{parentName:"p"},":"),"."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"router.add(\n 'user-detail',\n // highlight-next-line\n '/profile/:?userId',\n UserController,\n UserView\n);\n")),(0,a.yg)("admonition",{type:"caution"},(0,a.yg)("p",{parentName:"admonition"},"Optional parameters can be ",(0,a.yg)("strong",{parentName:"p"},"placed only after the last slash"),". Doing otherwise can cause unexpected behavior.")),(0,a.yg)("h2",{id:"linking-between-routes"},"Linking between routes"),(0,a.yg)("p",null,"URLs to routes can be generated via the ",(0,a.yg)("inlineCode",{parentName:"p"},"Router.link()")," public method. These can be then used in ordinary anchor tags and IMA.js makes sure, ",(0,a.yg)("strong",{parentName:"p"},"to handle the site routing in SPA mode"),", rather than doing redirect/reload of the whole page."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-jsx"},"import { AbstractComponent } from '@ima/react-page-renderer';\n\nclass OrderView extends AbstractComponent {\n render() {\n const { user, order } = this.props;\n\n const orderLink = this.link('order-detail', {\n userId: user.id,\n orderId: order.id\n });\n\n return View order\n }\n}\n")),(0,a.yg)("p",null,"This is done by listening to window ",(0,a.yg)("inlineCode",{parentName:"p"},"popstate")," and ",(0,a.yg)("inlineCode",{parentName:"p"},"click")," events and reacting accordingly (in the ",(0,a.yg)("inlineCode",{parentName:"p"},"listen")," method of ",(0,a.yg)("a",{parentName:"p",href:"https://github.com/seznam/ima/blob/next/packages/core/src/router/ClientRouter.js#L113"},"ClientRouter"),", which is called by IMA.js on client during app init). If the handled URL is not valid registered app route, it is handled normally (e.g you are redirected to the target URL)."),(0,a.yg)("admonition",{type:"tip"},(0,a.yg)("p",{parentName:"admonition"},"You can use ",(0,a.yg)("inlineCode",{parentName:"p"},"this.link")," helper method in IMA.js abstract component or the ",(0,a.yg)("inlineCode",{parentName:"p"},"useLink")," hook from the ",(0,a.yg)("a",{parentName:"p",href:"https://github.com/seznam/IMA.js-plugins/tree/master/packages/react-hooks"},"@ima/react-hooks")," plugin in your components and views to generate router links.")),(0,a.yg)("admonition",{type:"note"},(0,a.yg)("p",{parentName:"admonition"},"Under the hood, ",(0,a.yg)("inlineCode",{parentName:"p"},"this.link()")," is only alias for ",(0,a.yg)("inlineCode",{parentName:"p"},"this.utils.$Router.link"),", where ",(0,a.yg)("inlineCode",{parentName:"p"},"this.utils")," is taken from ",(0,a.yg)("inlineCode",{parentName:"p"},"this.context.$Utils"),"."),(0,a.yg)("p",{parentName:"admonition"},"For more information about ",(0,a.yg)("inlineCode",{parentName:"p"},"this.utils")," and ",(0,a.yg)("inlineCode",{parentName:"p"},"$Utils")," objects, take a look at the ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/rendering-process#react-context"},"React Context")," in the documentation.")),(0,a.yg)("h3",{id:"generating-links-outside-of-app-components"},"Generating links outside of app components"),(0,a.yg)("p",null,"Linking in ",(0,a.yg)("strong",{parentName:"p"},"Controllers"),", ",(0,a.yg)("strong",{parentName:"p"},"Extensions"),", ",(0,a.yg)("strong",{parentName:"p"},"Helpers")," and other ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container")," classes requires you to import ",(0,a.yg)("inlineCode",{parentName:"p"},"Router")," using ",(0,a.yg)("a",{parentName:"p",href:"/basic-features/object-container#1-dependency-injection"},"dependency injection"),". To do that you can either use ",(0,a.yg)("inlineCode",{parentName:"p"},"Router")," class in the dependency array, or ",(0,a.yg)("inlineCode",{parentName:"p"},"$Router")," string alias:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractController } from '@ima/core';\n\nexport default class DetailController extends AbstractController {\n static get $dependencies() {\n return ['$Router'];\n }\n\n constructor(router) {\n this._router = router;\n }\n\n load() {\n // ...\n }\n}\n")),(0,a.yg)("p",null,"Then you get ",(0,a.yg)("inlineCode",{parentName:"p"},"Router")," instance as the constructor's first argument, which gives you access to it's ",(0,a.yg)("inlineCode",{parentName:"p"},"link")," public method (and many others), that you can use to generate your desired route URL:"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"load() {\n const detailLink = this._router.link('order-detail', {\n userId: user.id,\n orderId: order.id\n });\n\n return { detailLink };\n}\n")),(0,a.yg)("h2",{id:"error-and-notfound-route-names"},"Error and NotFound route names"),(0,a.yg)("p",null,"There are two special route names that ",(0,a.yg)("inlineCode",{parentName:"p"},"@ima/core")," exports: ",(0,a.yg)("inlineCode",{parentName:"p"},"RouteNames.ERROR"),", ",(0,a.yg)("inlineCode",{parentName:"p"},"RouteNames.NOT_FOUND"),". You can use these constants to provide custom views and controllers for error handling pages."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript",metastring:"title=./app/config/routes.js",title:"./app/config/routes.js"},"import { RouteNames } from '@ima/core';\n\nimport { ErrorController, ErrorView } from 'app/page/error';\nimport { NotFoundController, NotFoundView } from 'app/page/not-found';\n\nexport let init = (ns, oc, config) => {\n const router = oc.get('$Router');\n\n router\n .add('home', '/', HomeController, HomeView)\n .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)\n .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);\n}\n")),(0,a.yg)("h2",{id:"redirects"},"Redirects"),(0,a.yg)("p",null,"In addition to the ",(0,a.yg)("inlineCode",{parentName:"p"},"link")," method mentioned above (which handles URL generation for given routes), you can use ",(0,a.yg)("inlineCode",{parentName:"p"},"Router.redirect()")," method to ",(0,a.yg)("strong",{parentName:"p"},"redirect directly to the targeted URL"),"."),(0,a.yg)("p",null,"This URL can be either existing app route or external URL. As with links, in this case you also get SPA routing, in case of redirection to different IMA.js app route."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractController, Router } from '@ima/core';\n\nexport default class DetailController extends AbstractController {\n static get $dependencies() {\n return [\n Router // We're using class descriptor in this case for DI\n ];\n }\n\n constructor(router) {\n this._router = router;\n }\n\n init() {\n // highlight-next-line\n this._router.redirect(\n // highlight-next-line\n this._router.link('order-detail', {\n // highlight-next-line\n userId: user.id,\n // highlight-next-line\n orderId: order.id\n // highlight-next-line\n });\n // highlight-next-line\n );\n }\n}\n")),(0,a.yg)("admonition",{type:"info"},(0,a.yg)("p",{parentName:"admonition"},"On client side, redirections are handled by simply changing the ",(0,a.yg)("inlineCode",{parentName:"p"},"window.location.href"),", while on server you're using the express native ",(0,a.yg)("inlineCode",{parentName:"p"},"res.redirect")," method.")),(0,a.yg)("h3",{id:"method-signature"},"Method signature"),(0,a.yg)("p",null,"The redirect method has following signature, while the options object is ",(0,a.yg)("strong",{parentName:"p"},"available only on server side"),":"),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"redirect(\n url = '',\n options = {} // Available only on server side\n)\n")),(0,a.yg)("h3",{id:"url"},"url"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"string"))),(0,a.yg)("p",null,"Target redirect URL."),(0,a.yg)("h3",{id:"options-1"},"options"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"object = {}"))),(0,a.yg)("p",null,"Additional options, used to customize redirect server response."),(0,a.yg)("pre",null,(0,a.yg)("code",{parentName:"pre",className:"language-javascript"},"{\n httpStatus: 302,\n headers: undefined,\n}\n")),(0,a.yg)("h4",{id:"httpstatus"},"httpStatus"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"number = 302"))),(0,a.yg)("p",null,"Custom redirect http status code."),(0,a.yg)("h4",{id:"headers"},"headers"),(0,a.yg)("blockquote",null,(0,a.yg)("p",{parentName:"blockquote"},(0,a.yg)("inlineCode",{parentName:"p"},"object = undefined"))),(0,a.yg)("p",null,"Custom response headers."))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/284c0bdc.4286649f.js b/assets/js/284c0bdc.97547500.js similarity index 99% rename from assets/js/284c0bdc.4286649f.js rename to assets/js/284c0bdc.97547500.js index a9392998b..c6f5e8b90 100644 --- a/assets/js/284c0bdc.4286649f.js +++ b/assets/js/284c0bdc.97547500.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[3007],{5680:(e,n,t)=>{t.d(n,{xA:()=>c,yg:()=>d});var a=t(6540);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var l=a.createContext({}),p=function(e){var n=a.useContext(l),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},c=function(e){var n=p(e.components);return a.createElement(l.Provider,{value:n},e.children)},g="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},m=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),g=p(t),m=r,d=g["".concat(l,".").concat(m)]||g[m]||u[m]||o;return t?a.createElement(d,i(i({ref:n},c),{},{components:t})):a.createElement(d,i({ref:n},c))}));function d(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,i=new Array(o);i[0]=m;var s={};for(var l in n)hasOwnProperty.call(n,l)&&(s[l]=n[l]);s.originalType=e,s[g]="string"==typeof e?e:r,i[1]=s;for(var p=2;p{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>s,toc:()=>p});var a=t(8102),r=(t(6540),t(5680));const o={title:"Dictionary",description:"Basic features > Dictionary and language features"},i=void 0,s={unversionedId:"basic-features/dictionary",id:"basic-features/dictionary",title:"Dictionary",description:"Basic features > Dictionary and language features",source:"@site/../docs/basic-features/dictionary.md",sourceDirName:"basic-features",slug:"/basic-features/dictionary",permalink:"/basic-features/dictionary",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/dictionary.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Dictionary",description:"Basic features > Dictionary and language features"},sidebar:"docs",previous:{title:"SEO & Meta Manager",permalink:"/basic-features/seo-and-meta-manager"},next:{title:"Error Handling",permalink:"/basic-features/error-handling"}},l={},p=[{value:"Configuration",id:"configuration",level:2},{value:"URL parser configuration",id:"url-parser-configuration",level:3},{value:":language placeholder",id:"language-placeholder",level:4},{value:"Language files",id:"language-files",level:3},{value:"Usage",id:"usage",level:2},{value:"Messageformat library",id:"messageformat-library",level:2}],c={toc:p},g="wrapper";function u(e){let{components:n,...t}=e;return(0,r.yg)(g,(0,a.A)({},c,t,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Dictionary in IMA.js app serves many purposes. Simplest of them is keeping text strings out of component markup. More advanced one would be internationalization and in-text replacements."),(0,r.yg)("h2",{id:"configuration"},"Configuration"),(0,r.yg)("p",null,"First we need to tell IMA.js where to look for dictionary files. Naming convention of the files is up to you, but it should be clear what language are the files meant for and ",(0,r.yg)("strong",{parentName:"p"},"glob\xa0pattern")," has to be able to match path to the files. IMA.js ",(0,r.yg)("strong",{parentName:"p"},"defaults to the following configuration:")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"languages: {\n cs: ['./app/**/*CS.json'],\n en: ['./app/**/*EN.json']\n}\n")),(0,r.yg)("p",null,"However you can easily override this settings in ",(0,r.yg)("a",{parentName:"p",href:"/cli/ima-config-js#languages"},"ima.config.js")," (an example):"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:'title="./ima.config.js"',title:'"./ima.config.js"'},"module.exports = {\n languages: {\n cs: [\n './app/component/**/*CS.json',\n './app/page/**/*CS.json'\n ],\n en: [\n './app/component/**/*EN.json',\n './app/page/**/*EN.json'\n ],\n de: [\n './app/component/**/*DE.json',\n './app/page/**/*DE.json'\n ]\n }\n}\n")),(0,r.yg)("h3",{id:"url-parser-configuration"},"URL parser configuration"),(0,r.yg)("p",null,"We also need to specify what language should be loaded. This is ",(0,r.yg)("strong",{parentName:"p"},"done dynamically depending on current URL"),". You can customize the URL patterns to language mapping in ",(0,r.yg)("strong",{parentName:"p"},"environment settings"),"."),(0,r.yg)("p",null,"The configuration consists of simple key-value pairs, that are used for configuring the languages used with specific hosts or starting paths:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},(0,r.yg)("inlineCode",{parentName:"strong"},"key"))," - has to start with '//' instead of a protocol, and you can define the root path."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},(0,r.yg)("inlineCode",{parentName:"strong"},"value"))," - is a language to use when the key is matched by the current URL.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js",metastring:'title="./server/config/environment.js"',title:'"./server/config/environment.js"'},"module.exports = (() => ({\n prod: {\n $Language: {\n '//*:*/cs': 'cs', // https://ima-app.com/cs/custom-route\n '//*:*/en': 'en', // https://ima-app.com/en/custom-route\n '//*:*': 'cs', // https://ima-app.com/custom-route\n },\n }\n}))();\n")),(0,r.yg)("h4",{id:"language-placeholder"},(0,r.yg)("inlineCode",{parentName:"h4"},":language")," placeholder"),(0,r.yg)("p",null,"To make the language definition a bit easier for multilingua applications, you can use ",(0,r.yg)("inlineCode",{parentName:"p"},":language")," placeholder in following way:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js",metastring:'title="./server/config/environment.js"',title:'"./server/config/environment.js"'},"module.exports = (() => ({\n prod: {\n $Language: {\n '//*:*/:language': ':language', // https://ima-app.com/[en|cs]/custom-route\n '//*:*': 'cs', // https://ima-app.com/custom-route\n },\n }\n}))();\n")),(0,r.yg)("h3",{id:"language-files"},"Language files"),(0,r.yg)("p",null,"The ",(0,r.yg)("a",{parentName:"p",href:"http://messageformat.github.io/messageformat/"},"messageformat")," compiler, which processes our language files, expects .JSON files on the input. Contents of these files are objects, which can be nested into multiple levels. These levels are then represtend as a namespace ",(0,r.yg)("inlineCode",{parentName:"p"},"key")," to the value in the dictionary."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js",metastring:"title=./pollVoteEN.json",title:"./pollVoteEN.json"},'{\n "resultTitle": "Result of {name}:",\n "result": {\n "voted": "{count, plural, =0{Found no results} one{Found one result} other{Found # results} }",\n "reader": "{gender, select, male{He said} female{She said} other{They said} }",\n }\n}\n')),(0,r.yg)("admonition",{type:"info"},(0,r.yg)("p",{parentName:"admonition"},"File name is used as a namespace for strings it defines. String defined under key ",(0,r.yg)("inlineCode",{parentName:"p"},"submit")," in file ",(0,r.yg)("inlineCode",{parentName:"p"},"uploadFormCS.json")," will be accessible under ",(0,r.yg)("inlineCode",{parentName:"p"},"uploadForm.submit"),".")),(0,r.yg)("h2",{id:"usage"},"Usage"),(0,r.yg)("p",null,"Every component and view extending ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractComponent")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractPureComponent")," has access to ",(0,r.yg)("inlineCode",{parentName:"p"},"localize")," method from within its instance. This method is alias to a ",(0,r.yg)("inlineCode",{parentName:"p"},"get")," method from the Dictionary instance and takes 2 arguments:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},"key")," - namespace and name of the localization string -> if you have ",(0,r.yg)("inlineCode",{parentName:"li"},"resultTitle")," string in file ",(0,r.yg)("inlineCode",{parentName:"li"},"pollVoteEN.json")," the key to this string would be ",(0,r.yg)("inlineCode",{parentName:"li"},"pollVote.resultTitle"),"."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},"parameters")," - Optional object with replacements and parameters for ",(0,r.yg)("a",{parentName:"li",href:"http://messageformat.github.io/messageformat/"},"messageformat")," syntax. For more info about the syntax check out ",(0,r.yg)("a",{parentName:"li",href:"http://userguide.icu-project.org/formatparse/messages"},"ICU guide"),".")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractPureComponent } from '@ima/react-page-renderer';\n\nclass PollVote extends AbstractPureComponent {\n render() {\n return (\n
\n {this.localize('pollVote.resultTitle')}\n {this.localize('pollVote.result.voted', {count: 3})}\n
\n );\n }\n}\n")),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"Use ",(0,r.yg)("inlineCode",{parentName:"p"},"useComponent().localize")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"useLocalize()")," hooks in functional components."),(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-jsx"},"import { useComponent, useLocalize } from '@ima/react-page-renderer';\n\nfunction PollVote() {\n // const { localize } = useComponent();\n const localize = useLocalize();\n\n return (\n
\n {localize('pollVote.resultTitle')}\n {localize('pollVote.result.voted', {count: 3})}\n
\n );\n}\n"))),(0,r.yg)("h2",{id:"messageformat-library"},"Messageformat library"),(0,r.yg)("p",null,"For more information on the available selectors, formatters, and other details, please see ",(0,r.yg)("a",{parentName:"p",href:"http://messageformat.github.io/messageformat/guide/"},"Format guide"),"."),(0,r.yg)("p",null,"Dictionary is also registered in ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container")," and thus can be obtained in Controllers, Extensions and other classes constructed through OC."))}u.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[3007],{5680:(e,n,t)=>{t.d(n,{xA:()=>c,yg:()=>d});var a=t(6540);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var l=a.createContext({}),p=function(e){var n=a.useContext(l),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},c=function(e){var n=p(e.components);return a.createElement(l.Provider,{value:n},e.children)},g="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},m=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),g=p(t),m=r,d=g["".concat(l,".").concat(m)]||g[m]||u[m]||o;return t?a.createElement(d,i(i({ref:n},c),{},{components:t})):a.createElement(d,i({ref:n},c))}));function d(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,i=new Array(o);i[0]=m;var s={};for(var l in n)hasOwnProperty.call(n,l)&&(s[l]=n[l]);s.originalType=e,s[g]="string"==typeof e?e:r,i[1]=s;for(var p=2;p{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>s,toc:()=>p});var a=t(8102),r=(t(6540),t(5680));const o={title:"Dictionary",description:"Basic features > Dictionary and language features"},i=void 0,s={unversionedId:"basic-features/dictionary",id:"basic-features/dictionary",title:"Dictionary",description:"Basic features > Dictionary and language features",source:"@site/../docs/basic-features/dictionary.md",sourceDirName:"basic-features",slug:"/basic-features/dictionary",permalink:"/basic-features/dictionary",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/dictionary.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Dictionary",description:"Basic features > Dictionary and language features"},sidebar:"docs",previous:{title:"SEO & Meta Manager",permalink:"/basic-features/seo-and-meta-manager"},next:{title:"Error Handling",permalink:"/basic-features/error-handling"}},l={},p=[{value:"Configuration",id:"configuration",level:2},{value:"URL parser configuration",id:"url-parser-configuration",level:3},{value:":language placeholder",id:"language-placeholder",level:4},{value:"Language files",id:"language-files",level:3},{value:"Usage",id:"usage",level:2},{value:"Messageformat library",id:"messageformat-library",level:2}],c={toc:p},g="wrapper";function u(e){let{components:n,...t}=e;return(0,r.yg)(g,(0,a.A)({},c,t,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Dictionary in IMA.js app serves many purposes. Simplest of them is keeping text strings out of component markup. More advanced one would be internationalization and in-text replacements."),(0,r.yg)("h2",{id:"configuration"},"Configuration"),(0,r.yg)("p",null,"First we need to tell IMA.js where to look for dictionary files. Naming convention of the files is up to you, but it should be clear what language are the files meant for and ",(0,r.yg)("strong",{parentName:"p"},"glob\xa0pattern")," has to be able to match path to the files. IMA.js ",(0,r.yg)("strong",{parentName:"p"},"defaults to the following configuration:")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"languages: {\n cs: ['./app/**/*CS.json'],\n en: ['./app/**/*EN.json']\n}\n")),(0,r.yg)("p",null,"However you can easily override this settings in ",(0,r.yg)("a",{parentName:"p",href:"/cli/ima-config-js#languages"},"ima.config.js")," (an example):"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript",metastring:'title="./ima.config.js"',title:'"./ima.config.js"'},"module.exports = {\n languages: {\n cs: [\n './app/component/**/*CS.json',\n './app/page/**/*CS.json'\n ],\n en: [\n './app/component/**/*EN.json',\n './app/page/**/*EN.json'\n ],\n de: [\n './app/component/**/*DE.json',\n './app/page/**/*DE.json'\n ]\n }\n}\n")),(0,r.yg)("h3",{id:"url-parser-configuration"},"URL parser configuration"),(0,r.yg)("p",null,"We also need to specify what language should be loaded. This is ",(0,r.yg)("strong",{parentName:"p"},"done dynamically depending on current URL"),". You can customize the URL patterns to language mapping in ",(0,r.yg)("strong",{parentName:"p"},"environment settings"),"."),(0,r.yg)("p",null,"The configuration consists of simple key-value pairs, that are used for configuring the languages used with specific hosts or starting paths:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},(0,r.yg)("inlineCode",{parentName:"strong"},"key"))," - has to start with '//' instead of a protocol, and you can define the root path."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},(0,r.yg)("inlineCode",{parentName:"strong"},"value"))," - is a language to use when the key is matched by the current URL.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js",metastring:'title="./server/config/environment.js"',title:'"./server/config/environment.js"'},"module.exports = (() => ({\n prod: {\n $Language: {\n '//*:*/cs': 'cs', // https://ima-app.com/cs/custom-route\n '//*:*/en': 'en', // https://ima-app.com/en/custom-route\n '//*:*': 'cs', // https://ima-app.com/custom-route\n },\n }\n}))();\n")),(0,r.yg)("h4",{id:"language-placeholder"},(0,r.yg)("inlineCode",{parentName:"h4"},":language")," placeholder"),(0,r.yg)("p",null,"To make the language definition a bit easier for multilingua applications, you can use ",(0,r.yg)("inlineCode",{parentName:"p"},":language")," placeholder in following way:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js",metastring:'title="./server/config/environment.js"',title:'"./server/config/environment.js"'},"module.exports = (() => ({\n prod: {\n $Language: {\n '//*:*/:language': ':language', // https://ima-app.com/[en|cs]/custom-route\n '//*:*': 'cs', // https://ima-app.com/custom-route\n },\n }\n}))();\n")),(0,r.yg)("h3",{id:"language-files"},"Language files"),(0,r.yg)("p",null,"The ",(0,r.yg)("a",{parentName:"p",href:"http://messageformat.github.io/messageformat/"},"messageformat")," compiler, which processes our language files, expects .JSON files on the input. Contents of these files are objects, which can be nested into multiple levels. These levels are then represtend as a namespace ",(0,r.yg)("inlineCode",{parentName:"p"},"key")," to the value in the dictionary."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js",metastring:"title=./pollVoteEN.json",title:"./pollVoteEN.json"},'{\n "resultTitle": "Result of {name}:",\n "result": {\n "voted": "{count, plural, =0{Found no results} one{Found one result} other{Found # results} }",\n "reader": "{gender, select, male{He said} female{She said} other{They said} }",\n }\n}\n')),(0,r.yg)("admonition",{type:"info"},(0,r.yg)("p",{parentName:"admonition"},"File name is used as a namespace for strings it defines. String defined under key ",(0,r.yg)("inlineCode",{parentName:"p"},"submit")," in file ",(0,r.yg)("inlineCode",{parentName:"p"},"uploadFormCS.json")," will be accessible under ",(0,r.yg)("inlineCode",{parentName:"p"},"uploadForm.submit"),".")),(0,r.yg)("h2",{id:"usage"},"Usage"),(0,r.yg)("p",null,"Every component and view extending ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractComponent")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"AbstractPureComponent")," has access to ",(0,r.yg)("inlineCode",{parentName:"p"},"localize")," method from within its instance. This method is alias to a ",(0,r.yg)("inlineCode",{parentName:"p"},"get")," method from the Dictionary instance and takes 2 arguments:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},"key")," - namespace and name of the localization string -> if you have ",(0,r.yg)("inlineCode",{parentName:"li"},"resultTitle")," string in file ",(0,r.yg)("inlineCode",{parentName:"li"},"pollVoteEN.json")," the key to this string would be ",(0,r.yg)("inlineCode",{parentName:"li"},"pollVote.resultTitle"),"."),(0,r.yg)("li",{parentName:"ul"},(0,r.yg)("strong",{parentName:"li"},"parameters")," - Optional object with replacements and parameters for ",(0,r.yg)("a",{parentName:"li",href:"http://messageformat.github.io/messageformat/"},"messageformat")," syntax. For more info about the syntax check out ",(0,r.yg)("a",{parentName:"li",href:"http://userguide.icu-project.org/formatparse/messages"},"ICU guide"),".")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"import { AbstractPureComponent } from '@ima/react-page-renderer';\n\nclass PollVote extends AbstractPureComponent {\n render() {\n return (\n
\n {this.localize('pollVote.resultTitle')}\n {this.localize('pollVote.result.voted', {count: 3})}\n
\n );\n }\n}\n")),(0,r.yg)("admonition",{type:"tip"},(0,r.yg)("p",{parentName:"admonition"},"Use ",(0,r.yg)("inlineCode",{parentName:"p"},"useComponent().localize")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"useLocalize()")," hooks in functional components."),(0,r.yg)("pre",{parentName:"admonition"},(0,r.yg)("code",{parentName:"pre",className:"language-jsx"},"import { useComponent, useLocalize } from '@ima/react-page-renderer';\n\nfunction PollVote() {\n // const { localize } = useComponent();\n const localize = useLocalize();\n\n return (\n
\n {localize('pollVote.resultTitle')}\n {localize('pollVote.result.voted', {count: 3})}\n
\n );\n}\n"))),(0,r.yg)("h2",{id:"messageformat-library"},"Messageformat library"),(0,r.yg)("p",null,"For more information on the available selectors, formatters, and other details, please see ",(0,r.yg)("a",{parentName:"p",href:"http://messageformat.github.io/messageformat/guide/"},"Format guide"),"."),(0,r.yg)("p",null,"Dictionary is also registered in ",(0,r.yg)("a",{parentName:"p",href:"/basic-features/object-container"},"Object Container")," and thus can be obtained in Controllers, Extensions and other classes constructed through OC."))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/2ece5d09.3dd1195a.js b/assets/js/2ece5d09.fe6c6957.js similarity index 99% rename from assets/js/2ece5d09.3dd1195a.js rename to assets/js/2ece5d09.fe6c6957.js index 519ec6402..fc8af234c 100644 --- a/assets/js/2ece5d09.3dd1195a.js +++ b/assets/js/2ece5d09.fe6c6957.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[1894],{5680:(e,n,t)=>{t.d(n,{xA:()=>p,yg:()=>m});var a=t(6540);function l(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function r(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(l[t]=e[t]);return l}(e,n);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(l[t]=e[t])}return l}var u=a.createContext({}),s=function(e){var n=a.useContext(u),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},p=function(e){var n=s(e.components);return a.createElement(u.Provider,{value:n},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},g=a.forwardRef((function(e,n){var t=e.components,l=e.mdxType,r=e.originalType,u=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),c=s(t),g=l,m=c["".concat(u,".").concat(g)]||c[g]||d[g]||r;return t?a.createElement(m,i(i({ref:n},p),{},{components:t})):a.createElement(m,i({ref:n},p))}));function m(e,n){var t=arguments,l=n&&n.mdxType;if("string"==typeof e||l){var r=t.length,i=new Array(r);i[0]=g;var o={};for(var u in n)hasOwnProperty.call(n,u)&&(o[u]=n[u]);o.originalType=e,o[c]="string"==typeof e?e:l,i[1]=o;for(var s=2;s{t.d(n,{A:()=>i});var a=t(6540),l=t(8017);const r={tabItem:"tabItem_Ymn6"};function i(e){let{children:n,hidden:t,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,l.A)(r.tabItem,i),hidden:t},n)}},1253:(e,n,t)=>{t.d(n,{A:()=>k});var a=t(8102),l=t(6540),r=t(8017),i=t(3104),o=t(9519),u=t(7485),s=t(1682),p=t(9466);function c(e){return function(e){return l.Children.map(e,(e=>{if(!e||(0,l.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:n,label:t,attributes:a,default:l}}=e;return{value:n,label:t,attributes:a,default:l}}))}function d(e){const{values:n,children:t}=e;return(0,l.useMemo)((()=>{const e=n??c(t);return function(e){const n=(0,s.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function g(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function m(e){let{queryString:n=!1,groupId:t}=e;const a=(0,o.W6)(),r=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,u.aZ)(r),(0,l.useCallback)((e=>{if(!r)return;const n=new URLSearchParams(a.location.search);n.set(r,e),a.replace({...a.location,search:n.toString()})}),[r,a])]}function y(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,r=d(e),[i,o]=(0,l.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!g({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const a=t.find((e=>e.default))??t[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:n,tabValues:r}))),[u,s]=m({queryString:t,groupId:a}),[c,y]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,r]=(0,p.Dv)(t);return[a,(0,l.useCallback)((e=>{t&&r.set(e)}),[t,r])]}({groupId:a}),b=(()=>{const e=u??c;return g({value:e,tabValues:r})?e:null})();(0,l.useLayoutEffect)((()=>{b&&o(b)}),[b]);return{selectedValue:i,selectValue:(0,l.useCallback)((e=>{if(!g({value:e,tabValues:r}))throw new Error(`Can't select invalid tab value=${e}`);o(e),s(e),y(e)}),[s,y,r]),tabValues:r}}var b=t(2303);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function h(e){let{className:n,block:t,selectedValue:o,selectValue:u,tabValues:s}=e;const p=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const n=e.currentTarget,t=p.indexOf(n),a=s[t].value;a!==o&&(c(n),u(a))},g=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=p.indexOf(e.currentTarget)+1;n=p[t]??p[0];break}case"ArrowLeft":{const t=p.indexOf(e.currentTarget)-1;n=p[t]??p[p.length-1];break}}n?.focus()};return l.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":t},n)},s.map((e=>{let{value:n,label:t,attributes:i}=e;return l.createElement("li",(0,a.A)({role:"tab",tabIndex:o===n?0:-1,"aria-selected":o===n,key:n,ref:e=>p.push(e),onKeyDown:g,onClick:d},i,{className:(0,r.A)("tabs__item",f.tabItem,i?.className,{"tabs__item--active":o===n})}),t??n)})))}function v(e){let{lazy:n,children:t,selectedValue:a}=e;const r=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=r.find((e=>e.props.value===a));return e?(0,l.cloneElement)(e,{className:"margin-top--md"}):null}return l.createElement("div",{className:"margin-top--md"},r.map(((e,n)=>(0,l.cloneElement)(e,{key:n,hidden:e.props.value!==a}))))}function w(e){const n=y(e);return l.createElement("div",{className:(0,r.A)("tabs-container",f.tabList)},l.createElement(h,(0,a.A)({},e,n)),l.createElement(v,(0,a.A)({},e,n)))}function k(e){const n=(0,b.A)();return l.createElement(w,(0,a.A)({key:String(n)},e))}},5376:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>p,contentTitle:()=>u,default:()=>m,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var a=t(8102),l=(t(6540),t(5680)),r=t(1253),i=t(6185);const o={title:"Analyze Plugin",description:"CLI > CLI Plugins and their API > Analyze Plugin"},u=void 0,s={unversionedId:"cli/plugins/analyze-plugin",id:"cli/plugins/analyze-plugin",title:"Analyze Plugin",description:"CLI > CLI Plugins and their API > Analyze Plugin",source:"@site/../docs/cli/plugins/analyze-plugin.md",sourceDirName:"cli/plugins",slug:"/cli/plugins/analyze-plugin",permalink:"/cli/plugins/analyze-plugin",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/cli/plugins/analyze-plugin.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Analyze Plugin",description:"CLI > CLI Plugins and their API > Analyze Plugin"},sidebar:"docs",previous:{title:"CLI Plugins API",permalink:"/cli/cli-plugins-api"},next:{title:"ScrambleCSS Plugin",permalink:"/cli/plugins/scramble-css-plugin"}},p={},c=[{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"CLI Arguments",id:"cli-arguments",level:2},{value:"--analyze",id:"--analyze",level:3},{value:"Options",id:"options",level:2},{value:"open",id:"open",level:3},{value:"bundleStatsOptions",id:"bundlestatsoptions",level:3},{value:"bundleAnalyzerOptions",id:"bundleanalyzeroptions",level:3}],d={toc:c},g="wrapper";function m(e){let{components:n,...t}=e;return(0,l.yg)(g,(0,a.A)({},d,t,{components:n,mdxType:"MDXLayout"}),(0,l.yg)("p",null,"Pre-configures ",(0,l.yg)("a",{parentName:"p",href:"https://npmjs.com/package/bundle-stats-webpack-plugin"},"bundle-stats-webpack-plugin")," and ",(0,l.yg)("a",{parentName:"p",href:"https://npmjs.com/package/webpack-bundle-analyzer"},"webpack-bundle-analyzer")," webpack plugins for fast and easy bundle analyzing."),(0,l.yg)("p",null,"This plugin provides easy way to ",(0,l.yg)("strong",{parentName:"p"},"analyze webpack bundle"),". Apart from pre-configuring the forementioned plugins, it also outputs ",(0,l.yg)("inlineCode",{parentName:"p"},"stats.json")," file which can be used in multiple other online webpack bundle analyzer tools. For example:"),(0,l.yg)("ul",null,(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://alexkuz.github.io/webpack-chart/"},"Webpack Chart")," - interactive pie chart"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://chrisbateman.github.io/webpack-visualizer/"},"Webpack Visualizer")," - visualize and analyze bundle"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://webpack.jakoblind.no/optimize/"},"Bundle optimize helper")," - analyze and optimize bundle"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://statoscope.tech/"},"Statoscope")," - detailed webpack stats analyzer")),(0,l.yg)("admonition",{type:"note"},(0,l.yg)("p",{parentName:"admonition"},"The plugin also prints these links directly into the console when the build finishes, for easier access.")),(0,l.yg)("h2",{id:"installation"},"Installation"),(0,l.yg)(r.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,l.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-bash"},"npm install @ima/cli-plugin-analyze -D\n"))),(0,l.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add @ima/cli-plugin-analyze --dev\n"))),(0,l.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add @ima/cli-plugin-analyze -D\n")))),(0,l.yg)("h2",{id:"usage"},"Usage"),(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-js",metastring:"title=./ima.config.js",title:"./ima.config.js"},"const { AnalyzePlugin } = require('@ima/cli-plugin-analyze');\n\n/**\n * @type import('@ima/cli').ImaConfig\n */\nmodule.exports = {\n plugins: [new AnalyzePlugin()],\n};\n")),(0,l.yg)("h2",{id:"cli-arguments"},"CLI Arguments"),(0,l.yg)("h3",{id:"--analyze"},"--analyze"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"client |\xa0client.es | server"))),(0,l.yg)("p",null,"Run the ima build command with ",(0,l.yg)("inlineCode",{parentName:"p"},"--analyze")," argument and pick one of the three produced bundles you want to analyze. For example: ",(0,l.yg)("inlineCode",{parentName:"p"},"npx ima build --analyze=client"),"."),(0,l.yg)("h2",{id:"options"},"Options"),(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-ts"},"new AnalyzePlugin(options: {\n open?: boolean;\n bundleStatsOptions?: BundleStatsWebpackPlugin.Options;\n bundleAnalyzerOptions?: BundleAnalyzerPlugin.Options;\n});\n")),(0,l.yg)("h3",{id:"open"},"open"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,l.yg)("p",null,"Set to false if you don't want to automatically open the browser window with the html reports when the build finishes."),(0,l.yg)("h3",{id:"bundlestatsoptions"},"bundleStatsOptions"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"object"))),(0,l.yg)("p",null,"Pass any option that the ",(0,l.yg)("inlineCode",{parentName:"p"},"BundleStatsWebpackPlugin")," accepts. These are then merged with some of our custom defaults."),(0,l.yg)("h3",{id:"bundleanalyzeroptions"},"bundleAnalyzerOptions"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"object"))),(0,l.yg)("p",null,"Pass any option that the ",(0,l.yg)("inlineCode",{parentName:"p"},"BundleAnalyzerPlugin")," accepts. These are then merged with some of our custom defaults."))}m.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[1894],{5680:(e,n,t)=>{t.d(n,{xA:()=>p,yg:()=>m});var a=t(6540);function l(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function r(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(l[t]=e[t]);return l}(e,n);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(l[t]=e[t])}return l}var u=a.createContext({}),s=function(e){var n=a.useContext(u),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},p=function(e){var n=s(e.components);return a.createElement(u.Provider,{value:n},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},g=a.forwardRef((function(e,n){var t=e.components,l=e.mdxType,r=e.originalType,u=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),c=s(t),g=l,m=c["".concat(u,".").concat(g)]||c[g]||d[g]||r;return t?a.createElement(m,i(i({ref:n},p),{},{components:t})):a.createElement(m,i({ref:n},p))}));function m(e,n){var t=arguments,l=n&&n.mdxType;if("string"==typeof e||l){var r=t.length,i=new Array(r);i[0]=g;var o={};for(var u in n)hasOwnProperty.call(n,u)&&(o[u]=n[u]);o.originalType=e,o[c]="string"==typeof e?e:l,i[1]=o;for(var s=2;s{t.d(n,{A:()=>i});var a=t(6540),l=t(8017);const r={tabItem:"tabItem_Ymn6"};function i(e){let{children:n,hidden:t,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,l.A)(r.tabItem,i),hidden:t},n)}},1253:(e,n,t)=>{t.d(n,{A:()=>k});var a=t(8102),l=t(6540),r=t(8017),i=t(3104),o=t(9519),u=t(7485),s=t(1682),p=t(9466);function c(e){return function(e){return l.Children.map(e,(e=>{if(!e||(0,l.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:n,label:t,attributes:a,default:l}}=e;return{value:n,label:t,attributes:a,default:l}}))}function d(e){const{values:n,children:t}=e;return(0,l.useMemo)((()=>{const e=n??c(t);return function(e){const n=(0,s.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function g(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function m(e){let{queryString:n=!1,groupId:t}=e;const a=(0,o.W6)(),r=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,u.aZ)(r),(0,l.useCallback)((e=>{if(!r)return;const n=new URLSearchParams(a.location.search);n.set(r,e),a.replace({...a.location,search:n.toString()})}),[r,a])]}function y(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,r=d(e),[i,o]=(0,l.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!g({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const a=t.find((e=>e.default))??t[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:n,tabValues:r}))),[u,s]=m({queryString:t,groupId:a}),[c,y]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,r]=(0,p.Dv)(t);return[a,(0,l.useCallback)((e=>{t&&r.set(e)}),[t,r])]}({groupId:a}),b=(()=>{const e=u??c;return g({value:e,tabValues:r})?e:null})();(0,l.useLayoutEffect)((()=>{b&&o(b)}),[b]);return{selectedValue:i,selectValue:(0,l.useCallback)((e=>{if(!g({value:e,tabValues:r}))throw new Error(`Can't select invalid tab value=${e}`);o(e),s(e),y(e)}),[s,y,r]),tabValues:r}}var b=t(2303);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function h(e){let{className:n,block:t,selectedValue:o,selectValue:u,tabValues:s}=e;const p=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const n=e.currentTarget,t=p.indexOf(n),a=s[t].value;a!==o&&(c(n),u(a))},g=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=p.indexOf(e.currentTarget)+1;n=p[t]??p[0];break}case"ArrowLeft":{const t=p.indexOf(e.currentTarget)-1;n=p[t]??p[p.length-1];break}}n?.focus()};return l.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":t},n)},s.map((e=>{let{value:n,label:t,attributes:i}=e;return l.createElement("li",(0,a.A)({role:"tab",tabIndex:o===n?0:-1,"aria-selected":o===n,key:n,ref:e=>p.push(e),onKeyDown:g,onClick:d},i,{className:(0,r.A)("tabs__item",f.tabItem,i?.className,{"tabs__item--active":o===n})}),t??n)})))}function v(e){let{lazy:n,children:t,selectedValue:a}=e;const r=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=r.find((e=>e.props.value===a));return e?(0,l.cloneElement)(e,{className:"margin-top--md"}):null}return l.createElement("div",{className:"margin-top--md"},r.map(((e,n)=>(0,l.cloneElement)(e,{key:n,hidden:e.props.value!==a}))))}function w(e){const n=y(e);return l.createElement("div",{className:(0,r.A)("tabs-container",f.tabList)},l.createElement(h,(0,a.A)({},e,n)),l.createElement(v,(0,a.A)({},e,n)))}function k(e){const n=(0,b.A)();return l.createElement(w,(0,a.A)({key:String(n)},e))}},5376:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>p,contentTitle:()=>u,default:()=>m,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var a=t(8102),l=(t(6540),t(5680)),r=t(1253),i=t(6185);const o={title:"Analyze Plugin",description:"CLI > CLI Plugins and their API > Analyze Plugin"},u=void 0,s={unversionedId:"cli/plugins/analyze-plugin",id:"cli/plugins/analyze-plugin",title:"Analyze Plugin",description:"CLI > CLI Plugins and their API > Analyze Plugin",source:"@site/../docs/cli/plugins/analyze-plugin.md",sourceDirName:"cli/plugins",slug:"/cli/plugins/analyze-plugin",permalink:"/cli/plugins/analyze-plugin",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/cli/plugins/analyze-plugin.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Analyze Plugin",description:"CLI > CLI Plugins and their API > Analyze Plugin"},sidebar:"docs",previous:{title:"CLI Plugins API",permalink:"/cli/cli-plugins-api"},next:{title:"ScrambleCSS Plugin",permalink:"/cli/plugins/scramble-css-plugin"}},p={},c=[{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"CLI Arguments",id:"cli-arguments",level:2},{value:"--analyze",id:"--analyze",level:3},{value:"Options",id:"options",level:2},{value:"open",id:"open",level:3},{value:"bundleStatsOptions",id:"bundlestatsoptions",level:3},{value:"bundleAnalyzerOptions",id:"bundleanalyzeroptions",level:3}],d={toc:c},g="wrapper";function m(e){let{components:n,...t}=e;return(0,l.yg)(g,(0,a.A)({},d,t,{components:n,mdxType:"MDXLayout"}),(0,l.yg)("p",null,"Pre-configures ",(0,l.yg)("a",{parentName:"p",href:"https://npmjs.com/package/bundle-stats-webpack-plugin"},"bundle-stats-webpack-plugin")," and ",(0,l.yg)("a",{parentName:"p",href:"https://npmjs.com/package/webpack-bundle-analyzer"},"webpack-bundle-analyzer")," webpack plugins for fast and easy bundle analyzing."),(0,l.yg)("p",null,"This plugin provides easy way to ",(0,l.yg)("strong",{parentName:"p"},"analyze webpack bundle"),". Apart from pre-configuring the forementioned plugins, it also outputs ",(0,l.yg)("inlineCode",{parentName:"p"},"stats.json")," file which can be used in multiple other online webpack bundle analyzer tools. For example:"),(0,l.yg)("ul",null,(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://alexkuz.github.io/webpack-chart/"},"Webpack Chart")," - interactive pie chart"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://chrisbateman.github.io/webpack-visualizer/"},"Webpack Visualizer")," - visualize and analyze bundle"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://webpack.jakoblind.no/optimize/"},"Bundle optimize helper")," - analyze and optimize bundle"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("a",{parentName:"li",href:"https://statoscope.tech/"},"Statoscope")," - detailed webpack stats analyzer")),(0,l.yg)("admonition",{type:"note"},(0,l.yg)("p",{parentName:"admonition"},"The plugin also prints these links directly into the console when the build finishes, for easier access.")),(0,l.yg)("h2",{id:"installation"},"Installation"),(0,l.yg)(r.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,l.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-bash"},"npm install @ima/cli-plugin-analyze -D\n"))),(0,l.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-bash"},"yarn add @ima/cli-plugin-analyze --dev\n"))),(0,l.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-bash"},"pnpm add @ima/cli-plugin-analyze -D\n")))),(0,l.yg)("h2",{id:"usage"},"Usage"),(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-js",metastring:"title=./ima.config.js",title:"./ima.config.js"},"const { AnalyzePlugin } = require('@ima/cli-plugin-analyze');\n\n/**\n * @type import('@ima/cli').ImaConfig\n */\nmodule.exports = {\n plugins: [new AnalyzePlugin()],\n};\n")),(0,l.yg)("h2",{id:"cli-arguments"},"CLI Arguments"),(0,l.yg)("h3",{id:"--analyze"},"--analyze"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"client |\xa0client.es | server"))),(0,l.yg)("p",null,"Run the ima build command with ",(0,l.yg)("inlineCode",{parentName:"p"},"--analyze")," argument and pick one of the three produced bundles you want to analyze. For example: ",(0,l.yg)("inlineCode",{parentName:"p"},"npx ima build --analyze=client"),"."),(0,l.yg)("h2",{id:"options"},"Options"),(0,l.yg)("pre",null,(0,l.yg)("code",{parentName:"pre",className:"language-ts"},"new AnalyzePlugin(options: {\n open?: boolean;\n bundleStatsOptions?: BundleStatsWebpackPlugin.Options;\n bundleAnalyzerOptions?: BundleAnalyzerPlugin.Options;\n});\n")),(0,l.yg)("h3",{id:"open"},"open"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"boolean = true"))),(0,l.yg)("p",null,"Set to false if you don't want to automatically open the browser window with the html reports when the build finishes."),(0,l.yg)("h3",{id:"bundlestatsoptions"},"bundleStatsOptions"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"object"))),(0,l.yg)("p",null,"Pass any option that the ",(0,l.yg)("inlineCode",{parentName:"p"},"BundleStatsWebpackPlugin")," accepts. These are then merged with some of our custom defaults."),(0,l.yg)("h3",{id:"bundleanalyzeroptions"},"bundleAnalyzerOptions"),(0,l.yg)("blockquote",null,(0,l.yg)("p",{parentName:"blockquote"},(0,l.yg)("inlineCode",{parentName:"p"},"object"))),(0,l.yg)("p",null,"Pass any option that the ",(0,l.yg)("inlineCode",{parentName:"p"},"BundleAnalyzerPlugin")," accepts. These are then merged with some of our custom defaults."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/2fa7fbb9.32b97aeb.js b/assets/js/2fa7fbb9.0e300ff9.js similarity index 99% rename from assets/js/2fa7fbb9.32b97aeb.js rename to assets/js/2fa7fbb9.0e300ff9.js index ad3dae1db..51246d96a 100644 --- a/assets/js/2fa7fbb9.32b97aeb.js +++ b/assets/js/2fa7fbb9.0e300ff9.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[920],{5680:(e,n,t)=>{t.d(n,{xA:()=>u,yg:()=>d});var a=t(6540);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var s=a.createContext({}),p=function(e){var n=a.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},u=function(e){var n=p(e.components);return a.createElement(s.Provider,{value:n},e.children)},c="mdxType",g={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},m=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),c=p(t),m=r,d=c["".concat(s,".").concat(m)]||c[m]||g[m]||o;return t?a.createElement(d,i(i({ref:n},u),{},{components:t})):a.createElement(d,i({ref:n},u))}));function d(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,i=new Array(o);i[0]=m;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[c]="string"==typeof e?e:r,i[1]=l;for(var p=2;p{t.d(n,{A:()=>i});var a=t(6540),r=t(8017);const o={tabItem:"tabItem_Ymn6"};function i(e){let{children:n,hidden:t,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,r.A)(o.tabItem,i),hidden:t},n)}},1253:(e,n,t)=>{t.d(n,{A:()=>N});var a=t(8102),r=t(6540),o=t(8017),i=t(3104),l=t(9519),s=t(7485),p=t(1682),u=t(9466);function c(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:n,label:t,attributes:a,default:r}}=e;return{value:n,label:t,attributes:a,default:r}}))}function g(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??c(t);return function(e){const n=(0,p.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function m(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function d(e){let{queryString:n=!1,groupId:t}=e;const a=(0,l.W6)(),o=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,s.aZ)(o),(0,r.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(a.location.search);n.set(o,e),a.replace({...a.location,search:n.toString()})}),[o,a])]}function y(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,o=g(e),[i,l]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const a=t.find((e=>e.default))??t[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:n,tabValues:o}))),[s,p]=d({queryString:t,groupId:a}),[c,y]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,o]=(0,u.Dv)(t);return[a,(0,r.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:a}),f=(()=>{const e=s??c;return m({value:e,tabValues:o})?e:null})();(0,r.useLayoutEffect)((()=>{f&&l(f)}),[f]);return{selectedValue:i,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),p(e),y(e)}),[p,y,o]),tabValues:o}}var f=t(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function h(e){let{className:n,block:t,selectedValue:l,selectValue:s,tabValues:p}=e;const u=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),g=e=>{const n=e.currentTarget,t=u.indexOf(n),a=p[t].value;a!==l&&(c(n),s(a))},m=e=>{let n=null;switch(e.key){case"Enter":g(e);break;case"ArrowRight":{const t=u.indexOf(e.currentTarget)+1;n=u[t]??u[0];break}case"ArrowLeft":{const t=u.indexOf(e.currentTarget)-1;n=u[t]??u[u.length-1];break}}n?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":t},n)},p.map((e=>{let{value:n,label:t,attributes:i}=e;return r.createElement("li",(0,a.A)({role:"tab",tabIndex:l===n?0:-1,"aria-selected":l===n,key:n,ref:e=>u.push(e),onKeyDown:m,onClick:g},i,{className:(0,o.A)("tabs__item",b.tabItem,i?.className,{"tabs__item--active":l===n})}),t??n)})))}function v(e){let{lazy:n,children:t,selectedValue:a}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===a));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},o.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==a}))))}function j(e){const n=y(e);return r.createElement("div",{className:(0,o.A)("tabs-container",b.tabList)},r.createElement(h,(0,a.A)({},e,n)),r.createElement(v,(0,a.A)({},e,n)))}function N(e){const n=(0,f.A)();return r.createElement(j,(0,a.A)({key:String(n)},e))}},2936:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>u,contentTitle:()=>s,default:()=>d,frontMatter:()=>l,metadata:()=>p,toc:()=>c});var a=t(8102),r=(t(6540),t(5680)),o=t(1253),i=t(6185);const l={title:"Migration 17.0.0",description:"Migration > Migration to version 17.0.0"},s=void 0,p={unversionedId:"migration/migration-17.0.0",id:"migration/migration-17.0.0",title:"Migration 17.0.0",description:"Migration > Migration to version 17.0.0",source:"@site/../docs/migration/migration-17.0.0.md",sourceDirName:"migration",slug:"/migration/migration-17.0.0",permalink:"/migration/migration-17.0.0",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/migration/migration-17.0.0.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Migration 17.0.0",description:"Migration > Migration to version 17.0.0"},sidebar:"docs",previous:{title:"Migration 0.16.0",permalink:"/migration/migration-0.16.0"},next:{title:"Migration 18.0.0",permalink:"/migration/migration-18.0.0"}},u={},c=[{value:"Imports",id:"imports",level:2},{value:"Context API",id:"context-api",level:2},{value:"Utils Registration",id:"utils-registration",level:2},{value:"IMA.js bundle for client/server",id:"imajs-bundle-for-clientserver",level:2},{value:"Language Key in Config",id:"language-key-in-config",level:2},{value:"Hot Reload",id:"hot-reload",level:2},{value:"IMA.js Plugins",id:"imajs-plugins",level:2}],g={toc:c},m="wrapper";function d(e){let{components:n,...t}=e;return(0,r.yg)(m,(0,a.A)({},g,t,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"IMA.js brings few major breaking changes, notably in the renaming of all packages. We've tried to make this process as easy as possible\nthrough the provided jscodeshift transform scripts. For more information read below."),(0,r.yg)("h2",{id:"imports"},"Imports"),(0,r.yg)("p",null,"The ",(0,r.yg)("inlineCode",{parentName:"p"},"ima-")," packages (even plugins) has been renamed to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/")," scoped packages and ",(0,r.yg)("inlineCode",{parentName:"p"},"ima")," core package has been renamed to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core"),". The core package is now bundled with ",(0,r.yg)("a",{parentName:"p",href:"https://rollupjs.org/guide/en/"},"rollup"),", so you can no longer import a file from specific path (i.e. ",(0,r.yg)("inlineCode",{parentName:"p"},"import GenericError from 'ima/error/GenericError'"),"), but you can import it directly from ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core")," (i.e. ",(0,r.yg)("inlineCode",{parentName:"p"},"import { GenericError } from '@ima/core'"),")."),(0,r.yg)("p",null,"All of this can be done automatically for a whole project using following jscodeshift script."),(0,r.yg)(o.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n")))),(0,r.yg)("p",null,"Also replace paths which contain ",(0,r.yg)("inlineCode",{parentName:"p"},"ima")," to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core")," in ",(0,r.yg)("inlineCode",{parentName:"p"},"package.json")," (setupFiles in jest) and ",(0,r.yg)("inlineCode",{parentName:"p"},"server.js"),"."),(0,r.yg)("p",null,"Following packages have been renamed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"ima-gulp-task-loader -> @ima/gulp-task-loader\nima-gulp-tasks -> @ima/gulp-tasks\nima-helpers -> @ima/helpers\nima-server -> @ima/server\n")),(0,r.yg)("p",null,"Following packages have been removed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"ima-examples\nima-skeleton\n")),(0,r.yg)("p",null,"And as a replacement, following package has been created."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"create-ima-app\n")),(0,r.yg)("p",null,"Also all plugins have been renamed from ",(0,r.yg)("inlineCode",{parentName:"p"},"ima-plugin-*")," to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/plugin-*"),"."),(0,r.yg)("h2",{id:"context-api"},"Context API"),(0,r.yg)("p",null,"IMA.js v17 no longer uses ",(0,r.yg)("inlineCode",{parentName:"p"},"prop-types")," in ",(0,r.yg)("inlineCode",{parentName:"p"},"contextTypes")," of ",(0,r.yg)("inlineCode",{parentName:"p"},"React")," components. Instead, you should use ",(0,r.yg)("inlineCode",{parentName:"p"},"PageContext")," from ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core"),". Also, ",(0,r.yg)("inlineCode",{parentName:"p"},"prop-types")," has been removed from ",(0,r.yg)("inlineCode",{parentName:"p"},"IMA.js")," dependencies, so if you need it for some reason, make sure it is installed as a project dependency."),(0,r.yg)("p",null,(0,r.yg)("strong",{parentName:"p"},"Example:")),(0,r.yg)("p",null,"This is original IMA.js v16 code."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"import PropTypes from 'prop-types';\n\nexport default class MyComponent extends AbstractComponent {\n static get contextTypes() {\n return {\n $Utils: PropTypes.object,\n urlParams: PropTypes.object\n };\n }\n}\n")),(0,r.yg)("p",null,"This should be the new IMA.js v17 code."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"import { PageContext } from '@ima/core';\n\nexport default class MyComponent extends AbstractComponent {\n static get contextType() {\n return PageContext;\n }\n}\n")),(0,r.yg)("p",null,"All of this can be done automatically for a whole project using following jscodeshift script."),(0,r.yg)(o.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n")))),(0,r.yg)("h2",{id:"utils-registration"},"Utils Registration"),(0,r.yg)("p",null,"There is a new way of defining component utils. You can no longer define ",(0,r.yg)("inlineCode",{parentName:"p"},"oc.constant('$Utils', {...})")," in ",(0,r.yg)("inlineCode",{parentName:"p"},"app/conf/bind.js"),", you have to use ",(0,r.yg)("inlineCode",{parentName:"p"},"oc.get(ComponentUtils).register({...})")," instead. Also, following component utils are predefined by default, so you don't have to define them yourself."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"oc.get(ComponentUtils).register({\n $CssClasses: '$CssClasses',\n $Dictionary: Dictionary,\n $Dispatcher: Dispatcher,\n $EventBus: EventBus,\n $Helper: '$Helper',\n $Http: HttpAgent,\n $PageStateManager: PageStateManager,\n $Router: Router,\n $Settings: '$Settings',\n $Window: Window\n});\n")),(0,r.yg)("p",null,(0,r.yg)("strong",{parentName:"p"},"Example:")),(0,r.yg)("p",null,"Following definition of utils is no longer supported."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"oc.constant('$Utils', {\n $MyCustomHelper: oc.get(MyCustomHelper),\n ...\n});\n")),(0,r.yg)("p",null,"And must be replaced with following."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"oc.get(ComponentUtils).register({\n $MyCustomHelper: MyCustomHelper,\n ...\n});\n")),(0,r.yg)("h2",{id:"imajs-bundle-for-clientserver"},"IMA.js bundle for client/server"),(0,r.yg)("p",null,"IMA.js v17 comes bundled for server and client side. This means smaller bundle for clients. To benefit from this, you should update ",(0,r.yg)("inlineCode",{parentName:"p"},"vendors")," in your ",(0,r.yg)("inlineCode",{parentName:"p"},"app/build.js")," as following."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"let vendors = {\n- common: ['@ima/core'],\n+ common: [],\n\n- server: [],\n+ server: [{ '@ima/core': '@ima/core/dist/ima.server.cjs.js' }],\n\n- client: [],\n+ client: [{ '@ima/core': '@ima/core/dist/ima.client.cjs.js' }],\n\n test: []\n};\n")),(0,r.yg)("h2",{id:"language-key-in-config"},"Language Key in Config"),(0,r.yg)("p",null,"Config key ",(0,r.yg)("inlineCode",{parentName:"p"},"language")," (mostly used in ",(0,r.yg)("inlineCode",{parentName:"p"},"app/config/*.js")," boot methods) has been renamed to ",(0,r.yg)("inlineCode",{parentName:"p"},"$Language"),". You can search whole project for ",(0,r.yg)("inlineCode",{parentName:"p"},"config.language")," and replace it with ",(0,r.yg)("inlineCode",{parentName:"p"},"config.$Language"),", but most likely, it will be used only in ",(0,r.yg)("inlineCode",{parentName:"p"},"app/config/settings.js"),"."),(0,r.yg)("h2",{id:"hot-reload"},"Hot Reload"),(0,r.yg)("p",null,"Hot Reload has been rewritten and published as ima plugin. Old hot reloading will no longer work. You should delete ",(0,r.yg)("inlineCode",{parentName:"p"},"app/assets/js/hot.reload.js")," from your project, then install the plugin via ",(0,r.yg)("inlineCode",{parentName:"p"},"npm install --save-dev @ima/plugin-websocket @ima/plugin-hot-reload")," and add following lines to your ",(0,r.yg)("inlineCode",{parentName:"p"},"app/build.js"),"."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"// You can add this somewhere below the vendors variable initialization\nif (\n process.env.NODE_ENV === 'dev' ||\n process.env.NODE_ENV === 'development' ||\n process.env.NODE_ENV === undefined\n) {\n vendors.common.push('@ima/plugin-websocket');\n vendors.common.push('@ima/plugin-hot-reload');\n}\n")),(0,r.yg)("h2",{id:"imajs-plugins"},"IMA.js Plugins"),(0,r.yg)("p",null,"All IMA.js plugins need to be updated to the latest version. Older versions won't work."))}d.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[920],{5680:(e,n,t)=>{t.d(n,{xA:()=>u,yg:()=>d});var a=t(6540);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var s=a.createContext({}),p=function(e){var n=a.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},u=function(e){var n=p(e.components);return a.createElement(s.Provider,{value:n},e.children)},c="mdxType",g={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},m=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),c=p(t),m=r,d=c["".concat(s,".").concat(m)]||c[m]||g[m]||o;return t?a.createElement(d,i(i({ref:n},u),{},{components:t})):a.createElement(d,i({ref:n},u))}));function d(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,i=new Array(o);i[0]=m;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[c]="string"==typeof e?e:r,i[1]=l;for(var p=2;p{t.d(n,{A:()=>i});var a=t(6540),r=t(8017);const o={tabItem:"tabItem_Ymn6"};function i(e){let{children:n,hidden:t,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,r.A)(o.tabItem,i),hidden:t},n)}},1253:(e,n,t)=>{t.d(n,{A:()=>N});var a=t(8102),r=t(6540),o=t(8017),i=t(3104),l=t(9519),s=t(7485),p=t(1682),u=t(9466);function c(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:n,label:t,attributes:a,default:r}}=e;return{value:n,label:t,attributes:a,default:r}}))}function g(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??c(t);return function(e){const n=(0,p.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function m(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function d(e){let{queryString:n=!1,groupId:t}=e;const a=(0,l.W6)(),o=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,s.aZ)(o),(0,r.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(a.location.search);n.set(o,e),a.replace({...a.location,search:n.toString()})}),[o,a])]}function y(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,o=g(e),[i,l]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const a=t.find((e=>e.default))??t[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:n,tabValues:o}))),[s,p]=d({queryString:t,groupId:a}),[c,y]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,o]=(0,u.Dv)(t);return[a,(0,r.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:a}),f=(()=>{const e=s??c;return m({value:e,tabValues:o})?e:null})();(0,r.useLayoutEffect)((()=>{f&&l(f)}),[f]);return{selectedValue:i,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),p(e),y(e)}),[p,y,o]),tabValues:o}}var f=t(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function h(e){let{className:n,block:t,selectedValue:l,selectValue:s,tabValues:p}=e;const u=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),g=e=>{const n=e.currentTarget,t=u.indexOf(n),a=p[t].value;a!==l&&(c(n),s(a))},m=e=>{let n=null;switch(e.key){case"Enter":g(e);break;case"ArrowRight":{const t=u.indexOf(e.currentTarget)+1;n=u[t]??u[0];break}case"ArrowLeft":{const t=u.indexOf(e.currentTarget)-1;n=u[t]??u[u.length-1];break}}n?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":t},n)},p.map((e=>{let{value:n,label:t,attributes:i}=e;return r.createElement("li",(0,a.A)({role:"tab",tabIndex:l===n?0:-1,"aria-selected":l===n,key:n,ref:e=>u.push(e),onKeyDown:m,onClick:g},i,{className:(0,o.A)("tabs__item",b.tabItem,i?.className,{"tabs__item--active":l===n})}),t??n)})))}function v(e){let{lazy:n,children:t,selectedValue:a}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===a));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},o.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==a}))))}function j(e){const n=y(e);return r.createElement("div",{className:(0,o.A)("tabs-container",b.tabList)},r.createElement(h,(0,a.A)({},e,n)),r.createElement(v,(0,a.A)({},e,n)))}function N(e){const n=(0,f.A)();return r.createElement(j,(0,a.A)({key:String(n)},e))}},2936:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>u,contentTitle:()=>s,default:()=>d,frontMatter:()=>l,metadata:()=>p,toc:()=>c});var a=t(8102),r=(t(6540),t(5680)),o=t(1253),i=t(6185);const l={title:"Migration 17.0.0",description:"Migration > Migration to version 17.0.0"},s=void 0,p={unversionedId:"migration/migration-17.0.0",id:"migration/migration-17.0.0",title:"Migration 17.0.0",description:"Migration > Migration to version 17.0.0",source:"@site/../docs/migration/migration-17.0.0.md",sourceDirName:"migration",slug:"/migration/migration-17.0.0",permalink:"/migration/migration-17.0.0",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/migration/migration-17.0.0.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Migration 17.0.0",description:"Migration > Migration to version 17.0.0"},sidebar:"docs",previous:{title:"Migration 0.16.0",permalink:"/migration/migration-0.16.0"},next:{title:"Migration 18.0.0",permalink:"/migration/migration-18.0.0"}},u={},c=[{value:"Imports",id:"imports",level:2},{value:"Context API",id:"context-api",level:2},{value:"Utils Registration",id:"utils-registration",level:2},{value:"IMA.js bundle for client/server",id:"imajs-bundle-for-clientserver",level:2},{value:"Language Key in Config",id:"language-key-in-config",level:2},{value:"Hot Reload",id:"hot-reload",level:2},{value:"IMA.js Plugins",id:"imajs-plugins",level:2}],g={toc:c},m="wrapper";function d(e){let{components:n,...t}=e;return(0,r.yg)(m,(0,a.A)({},g,t,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"IMA.js brings few major breaking changes, notably in the renaming of all packages. We've tried to make this process as easy as possible\nthrough the provided jscodeshift transform scripts. For more information read below."),(0,r.yg)("h2",{id:"imports"},"Imports"),(0,r.yg)("p",null,"The ",(0,r.yg)("inlineCode",{parentName:"p"},"ima-")," packages (even plugins) has been renamed to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/")," scoped packages and ",(0,r.yg)("inlineCode",{parentName:"p"},"ima")," core package has been renamed to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core"),". The core package is now bundled with ",(0,r.yg)("a",{parentName:"p",href:"https://rollupjs.org/guide/en/"},"rollup"),", so you can no longer import a file from specific path (i.e. ",(0,r.yg)("inlineCode",{parentName:"p"},"import GenericError from 'ima/error/GenericError'"),"), but you can import it directly from ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core")," (i.e. ",(0,r.yg)("inlineCode",{parentName:"p"},"import { GenericError } from '@ima/core'"),")."),(0,r.yg)("p",null,"All of this can be done automatically for a whole project using following jscodeshift script."),(0,r.yg)(o.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n")))),(0,r.yg)("p",null,"Also replace paths which contain ",(0,r.yg)("inlineCode",{parentName:"p"},"ima")," to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core")," in ",(0,r.yg)("inlineCode",{parentName:"p"},"package.json")," (setupFiles in jest) and ",(0,r.yg)("inlineCode",{parentName:"p"},"server.js"),"."),(0,r.yg)("p",null,"Following packages have been renamed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"ima-gulp-task-loader -> @ima/gulp-task-loader\nima-gulp-tasks -> @ima/gulp-tasks\nima-helpers -> @ima/helpers\nima-server -> @ima/server\n")),(0,r.yg)("p",null,"Following packages have been removed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"ima-examples\nima-skeleton\n")),(0,r.yg)("p",null,"And as a replacement, following package has been created."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"create-ima-app\n")),(0,r.yg)("p",null,"Also all plugins have been renamed from ",(0,r.yg)("inlineCode",{parentName:"p"},"ima-plugin-*")," to ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/plugin-*"),"."),(0,r.yg)("h2",{id:"context-api"},"Context API"),(0,r.yg)("p",null,"IMA.js v17 no longer uses ",(0,r.yg)("inlineCode",{parentName:"p"},"prop-types")," in ",(0,r.yg)("inlineCode",{parentName:"p"},"contextTypes")," of ",(0,r.yg)("inlineCode",{parentName:"p"},"React")," components. Instead, you should use ",(0,r.yg)("inlineCode",{parentName:"p"},"PageContext")," from ",(0,r.yg)("inlineCode",{parentName:"p"},"@ima/core"),". Also, ",(0,r.yg)("inlineCode",{parentName:"p"},"prop-types")," has been removed from ",(0,r.yg)("inlineCode",{parentName:"p"},"IMA.js")," dependencies, so if you need it for some reason, make sure it is installed as a project dependency."),(0,r.yg)("p",null,(0,r.yg)("strong",{parentName:"p"},"Example:")),(0,r.yg)("p",null,"This is original IMA.js v16 code."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"import PropTypes from 'prop-types';\n\nexport default class MyComponent extends AbstractComponent {\n static get contextTypes() {\n return {\n $Utils: PropTypes.object,\n urlParams: PropTypes.object\n };\n }\n}\n")),(0,r.yg)("p",null,"This should be the new IMA.js v17 code."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"import { PageContext } from '@ima/core';\n\nexport default class MyComponent extends AbstractComponent {\n static get contextType() {\n return PageContext;\n }\n}\n")),(0,r.yg)("p",null,"All of this can be done automatically for a whole project using following jscodeshift script."),(0,r.yg)(o.A,{groupId:"npm2yarn",mdxType:"Tabs"},(0,r.yg)(i.A,{value:"npm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n"))),(0,r.yg)(i.A,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-bash"},"npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./\n")))),(0,r.yg)("h2",{id:"utils-registration"},"Utils Registration"),(0,r.yg)("p",null,"There is a new way of defining component utils. You can no longer define ",(0,r.yg)("inlineCode",{parentName:"p"},"oc.constant('$Utils', {...})")," in ",(0,r.yg)("inlineCode",{parentName:"p"},"app/conf/bind.js"),", you have to use ",(0,r.yg)("inlineCode",{parentName:"p"},"oc.get(ComponentUtils).register({...})")," instead. Also, following component utils are predefined by default, so you don't have to define them yourself."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"oc.get(ComponentUtils).register({\n $CssClasses: '$CssClasses',\n $Dictionary: Dictionary,\n $Dispatcher: Dispatcher,\n $EventBus: EventBus,\n $Helper: '$Helper',\n $Http: HttpAgent,\n $PageStateManager: PageStateManager,\n $Router: Router,\n $Settings: '$Settings',\n $Window: Window\n});\n")),(0,r.yg)("p",null,(0,r.yg)("strong",{parentName:"p"},"Example:")),(0,r.yg)("p",null,"Following definition of utils is no longer supported."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"oc.constant('$Utils', {\n $MyCustomHelper: oc.get(MyCustomHelper),\n ...\n});\n")),(0,r.yg)("p",null,"And must be replaced with following."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"oc.get(ComponentUtils).register({\n $MyCustomHelper: MyCustomHelper,\n ...\n});\n")),(0,r.yg)("h2",{id:"imajs-bundle-for-clientserver"},"IMA.js bundle for client/server"),(0,r.yg)("p",null,"IMA.js v17 comes bundled for server and client side. This means smaller bundle for clients. To benefit from this, you should update ",(0,r.yg)("inlineCode",{parentName:"p"},"vendors")," in your ",(0,r.yg)("inlineCode",{parentName:"p"},"app/build.js")," as following."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"let vendors = {\n- common: ['@ima/core'],\n+ common: [],\n\n- server: [],\n+ server: [{ '@ima/core': '@ima/core/dist/ima.server.cjs.js' }],\n\n- client: [],\n+ client: [{ '@ima/core': '@ima/core/dist/ima.client.cjs.js' }],\n\n test: []\n};\n")),(0,r.yg)("h2",{id:"language-key-in-config"},"Language Key in Config"),(0,r.yg)("p",null,"Config key ",(0,r.yg)("inlineCode",{parentName:"p"},"language")," (mostly used in ",(0,r.yg)("inlineCode",{parentName:"p"},"app/config/*.js")," boot methods) has been renamed to ",(0,r.yg)("inlineCode",{parentName:"p"},"$Language"),". You can search whole project for ",(0,r.yg)("inlineCode",{parentName:"p"},"config.language")," and replace it with ",(0,r.yg)("inlineCode",{parentName:"p"},"config.$Language"),", but most likely, it will be used only in ",(0,r.yg)("inlineCode",{parentName:"p"},"app/config/settings.js"),"."),(0,r.yg)("h2",{id:"hot-reload"},"Hot Reload"),(0,r.yg)("p",null,"Hot Reload has been rewritten and published as ima plugin. Old hot reloading will no longer work. You should delete ",(0,r.yg)("inlineCode",{parentName:"p"},"app/assets/js/hot.reload.js")," from your project, then install the plugin via ",(0,r.yg)("inlineCode",{parentName:"p"},"npm install --save-dev @ima/plugin-websocket @ima/plugin-hot-reload")," and add following lines to your ",(0,r.yg)("inlineCode",{parentName:"p"},"app/build.js"),"."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-js"},"// You can add this somewhere below the vendors variable initialization\nif (\n process.env.NODE_ENV === 'dev' ||\n process.env.NODE_ENV === 'development' ||\n process.env.NODE_ENV === undefined\n) {\n vendors.common.push('@ima/plugin-websocket');\n vendors.common.push('@ima/plugin-hot-reload');\n}\n")),(0,r.yg)("h2",{id:"imajs-plugins"},"IMA.js Plugins"),(0,r.yg)("p",null,"All IMA.js plugins need to be updated to the latest version. Older versions won't work."))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/327389ac.d2776e53.js b/assets/js/327389ac.563aa69f.js similarity index 98% rename from assets/js/327389ac.d2776e53.js rename to assets/js/327389ac.563aa69f.js index f30a49604..d36218e57 100644 --- a/assets/js/327389ac.d2776e53.js +++ b/assets/js/327389ac.563aa69f.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[1495],{2400:(e,t,a)=>{a.d(t,{A:()=>n});const n=a.p+"assets/images/diagram-page-state-5512b3a2b5f48555cf76c83172bf4788.png"},5680:(e,t,a)=>{a.d(t,{xA:()=>p,yg:()=>m});var n=a(6540);function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function s(e){for(var t=1;t=0||(r[a]=e[a]);return r}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var l=n.createContext({}),c=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},p=function(e){var t=c(e.components);return n.createElement(l.Provider,{value:t},e.children)},d="mdxType",g={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},u=n.forwardRef((function(e,t){var a=e.components,r=e.mdxType,o=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),d=c(a),u=r,m=d["".concat(l,".").concat(u)]||d[u]||g[u]||o;return a?n.createElement(m,s(s({ref:t},p),{},{components:a})):n.createElement(m,s({ref:t},p))}));function m(e,t){var a=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var o=a.length,s=new Array(o);s[0]=u;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[d]="string"==typeof e?e:r,s[1]=i;for(var c=2;c{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>g,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var n=a(8102),r=(a(6540),a(5680));const o={title:"Page State",description:"Basic features > Page State and it's usage"},s=void 0,i={unversionedId:"basic-features/page-state",id:"basic-features/page-state",title:"Page State",description:"Basic features > Page State and it's usage",source:"@site/../docs/basic-features/page-state.md",sourceDirName:"basic-features",slug:"/basic-features/page-state",permalink:"/basic-features/page-state",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/page-state.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Page State",description:"Basic features > Page State and it's usage"},sidebar:"docs",previous:{title:"Events",permalink:"/basic-features/events"},next:{title:"SEO & Meta Manager",permalink:"/basic-features/seo-and-meta-manager"}},l={},c=[{value:"Get &\xa0Set",id:"get-set",level:2},{value:"Initial page state",id:"initial-page-state",level:2},{value:"Partial state",id:"partial-state",level:2},{value:"State transactions",id:"state-transactions",level:2}],p={toc:c},d="wrapper";function g(e){let{components:t,...o}=e;return(0,r.yg)(d,(0,n.A)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Core of each application is the data the app is working with. The data needs to be managed in some manner and user needs to be able to manipulate with the data during application run. IMA.js adopted a React style of state management."),(0,r.yg)("p",null,(0,r.yg)("img",{src:a(2400).A,width:"881",height:"421"})),(0,r.yg)("p",null,"A ",(0,r.yg)("strong",{parentName:"p"},"PageStateManager")," class is used for managing ",(0,r.yg)("strong",{parentName:"p"},"page state")," and is in tight cooperation with ",(0,r.yg)("strong",{parentName:"p"},"PageManager"),".\nPageManager need state manager to collect initial state from Controller and registered extension, and to be informed about every state change that happens inside Controller or Extension."),(0,r.yg)("h2",{id:"get-set"},"Get &\xa0Set"),(0,r.yg)("p",null,"As we've mentioned before, IMA.js state management is inspired by React. In every Controller and Extension you can call ",(0,r.yg)("inlineCode",{parentName:"p"},"this.setState(patchObject)")," method that will update page state and trigger new rendering of a View. Counterpart to ",(0,r.yg)("inlineCode",{parentName:"p"},"setState")," is ",(0,r.yg)("inlineCode",{parentName:"p"},"getState"),". This method returns current state that is shared among controller and all its registered extensions."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/home/HomeController.js\n\nonVisibilityToggle() {\n const {\xa0visibility } = this.getState();\n\n this.setState({ visibility: !visibility });\n}\n")),(0,r.yg)("h2",{id:"initial-page-state"},"Initial page state"),(0,r.yg)("p",null,"First additions to page state are set when ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method of a Controller and Extensions returns an object of resources. These resources may be plain data or (un)resolved promises. Promises are handled differently on server vs. client. This behavior is described in Controller's ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#load-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"load")," method documentation"),"."),(0,r.yg)("h2",{id:"partial-state"},"Partial state"),(0,r.yg)("p",null,"Since Extensions also have a word in loading resources it may be necessary to share resources between Controller and Extensions. Here comes partial state into play. It allows you to call ",(0,r.yg)("inlineCode",{parentName:"p"},"getState")," method in ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method of an Extension. Received state consists of states collected from loaded Controller and Extensions loaded prior to the current Extension. Extensions are loaded in the same order as they were registered in a Controller."),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note"),": Promises in received state may not be resolved. Therefore you need to chain promises or use ",(0,r.yg)("inlineCode",{parentName:"p"},"async/await"),".")),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note"),": If you'll use ",(0,r.yg)("inlineCode",{parentName:"p"},"async/await")," execution will not be parallel relative to other promises.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/home/HomeController.js\nexport default class HomeController extends AbstractController {\n\n load() {\n const userPromise = this._userService.load(this.params.userId);\n\n return {\n user: userPromise\n };\n }\n}\n")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/component/poll/PollExtension.js\nexport default class PollExtension extends AbstractExtension {\n getAllowedStateKeys() {\n return ['pollVotes'];\n }\n\n load() {\n const { user: userPromise } = this.getState();\n const pollVotesPromise = userPromise.then(\n user => this._pollService.getVotes(user.id)\n );\n\n return {\n pollVotes: pollVotesPromise\n };\n }\n}\n")),(0,r.yg)("h2",{id:"state-transactions"},"State transactions"),(0,r.yg)("p",null,"State transactions, similarly to SQL transactions, provide a way to queue state patches and then commit them as a one to the original state."),(0,r.yg)("p",null,"They're here for use cases where you'd in you workflow call ",(0,r.yg)("inlineCode",{parentName:"p"},"setState")," method multiple times or you'd have to collect state patches in a separate variable (this is hard to do across multiple methods)."),(0,r.yg)("p",null,"Transaction is initiated with ",(0,r.yg)("inlineCode",{parentName:"p"},"beginStateTransaction()")," in Controller/Extension. After that\nevery setState call is queued and doesn't change the state or re-render anything. If there\nis another transaction initiated before you commit you'll lost your patches."),(0,r.yg)("p",null,"If you want to see what changes are in queue from the begin of transaction call ",(0,r.yg)("inlineCode",{parentName:"p"},"getTransactionStatePatches()")," method."),(0,r.yg)("p",null,"To finish the transaction you have to call ",(0,r.yg)("inlineCode",{parentName:"p"},"commitStateTransaction()")," method. It will squash\nall the patches made during the transaction into a one and apply it to the original state.\nTherefore your application will re-render only once and you'll also receive ",(0,r.yg)("a",{parentName:"p",href:"./events#stateeventsbefore_change_state"},"state events")," only once."),(0,r.yg)("p",null,"Another way to finish the transaction is to cancel it via ",(0,r.yg)("inlineCode",{parentName:"p"},"cancelStateTransaction()")," method."),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note"),": Call to ",(0,r.yg)("inlineCode",{parentName:"p"},"getState")," method after the transaction has begun will return state as it was before the transaction eg. the returned state doesn't include changes from the transaction period until the transaction is committed.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"async onFormSubmit({ content, deleteRevisions = false }) {\n const { article } = this.getState();\n\n this.beginStateTransaction();\n\n const result = await this._http.put(/* ... */);\n\n if (deleteRevisions) {\n await this.deleteArticleRevisions();\n }\n\n this.setState({ article: Object.assign({}, article, {\xa0content }) });\n this.commitStateTransaction();\n}\n\nasync deleteArticleRevisions() {\n const { article, revisions } = this.getState();\n\n await this._http.delete(/* ... */);\n\n this.setState({ revisions: [] });\n}\n")),(0,r.yg)("p",null,"In the example above, after the form is submitted with ",(0,r.yg)("inlineCode",{parentName:"p"},"deleteRevisions = true"),":"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"Two ",(0,r.yg)("inlineCode",{parentName:"li"},"setState")," calls are made"),(0,r.yg)("li",{parentName:"ul"},"Only one render is triggered after the ",(0,r.yg)("inlineCode",{parentName:"li"},"commitStateTransaction")," call")))}g.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[1495],{2400:(e,t,a)=>{a.d(t,{A:()=>n});const n=a.p+"assets/images/diagram-page-state-5512b3a2b5f48555cf76c83172bf4788.png"},5680:(e,t,a)=>{a.d(t,{xA:()=>p,yg:()=>m});var n=a(6540);function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function s(e){for(var t=1;t=0||(r[a]=e[a]);return r}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var l=n.createContext({}),c=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},p=function(e){var t=c(e.components);return n.createElement(l.Provider,{value:t},e.children)},d="mdxType",g={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},u=n.forwardRef((function(e,t){var a=e.components,r=e.mdxType,o=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),d=c(a),u=r,m=d["".concat(l,".").concat(u)]||d[u]||g[u]||o;return a?n.createElement(m,s(s({ref:t},p),{},{components:a})):n.createElement(m,s({ref:t},p))}));function m(e,t){var a=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var o=a.length,s=new Array(o);s[0]=u;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[d]="string"==typeof e?e:r,s[1]=i;for(var c=2;c{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>g,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var n=a(8102),r=(a(6540),a(5680));const o={title:"Page State",description:"Basic features > Page State and it's usage"},s=void 0,i={unversionedId:"basic-features/page-state",id:"basic-features/page-state",title:"Page State",description:"Basic features > Page State and it's usage",source:"@site/../docs/basic-features/page-state.md",sourceDirName:"basic-features",slug:"/basic-features/page-state",permalink:"/basic-features/page-state",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/basic-features/page-state.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1738143288,formattedLastUpdatedAt:"Jan 29, 2025",frontMatter:{title:"Page State",description:"Basic features > Page State and it's usage"},sidebar:"docs",previous:{title:"Events",permalink:"/basic-features/events"},next:{title:"SEO & Meta Manager",permalink:"/basic-features/seo-and-meta-manager"}},l={},c=[{value:"Get &\xa0Set",id:"get-set",level:2},{value:"Initial page state",id:"initial-page-state",level:2},{value:"Partial state",id:"partial-state",level:2},{value:"State transactions",id:"state-transactions",level:2}],p={toc:c},d="wrapper";function g(e){let{components:t,...o}=e;return(0,r.yg)(d,(0,n.A)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"Core of each application is the data the app is working with. The data needs to be managed in some manner and user needs to be able to manipulate with the data during application run. IMA.js adopted a React style of state management."),(0,r.yg)("p",null,(0,r.yg)("img",{src:a(2400).A,width:"881",height:"421"})),(0,r.yg)("p",null,"A ",(0,r.yg)("strong",{parentName:"p"},"PageStateManager")," class is used for managing ",(0,r.yg)("strong",{parentName:"p"},"page state")," and is in tight cooperation with ",(0,r.yg)("strong",{parentName:"p"},"PageManager"),".\nPageManager need state manager to collect initial state from Controller and registered extension, and to be informed about every state change that happens inside Controller or Extension."),(0,r.yg)("h2",{id:"get-set"},"Get &\xa0Set"),(0,r.yg)("p",null,"As we've mentioned before, IMA.js state management is inspired by React. In every Controller and Extension you can call ",(0,r.yg)("inlineCode",{parentName:"p"},"this.setState(patchObject)")," method that will update page state and trigger new rendering of a View. Counterpart to ",(0,r.yg)("inlineCode",{parentName:"p"},"setState")," is ",(0,r.yg)("inlineCode",{parentName:"p"},"getState"),". This method returns current state that is shared among controller and all its registered extensions."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/home/HomeController.js\n\nonVisibilityToggle() {\n const {\xa0visibility } = this.getState();\n\n this.setState({ visibility: !visibility });\n}\n")),(0,r.yg)("h2",{id:"initial-page-state"},"Initial page state"),(0,r.yg)("p",null,"First additions to page state are set when ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method of a Controller and Extensions returns an object of resources. These resources may be plain data or (un)resolved promises. Promises are handled differently on server vs. client. This behavior is described in Controller's ",(0,r.yg)("a",{parentName:"p",href:"./controller-lifecycle#load-serverclient"},(0,r.yg)("inlineCode",{parentName:"a"},"load")," method documentation"),"."),(0,r.yg)("h2",{id:"partial-state"},"Partial state"),(0,r.yg)("p",null,"Since Extensions also have a word in loading resources it may be necessary to share resources between Controller and Extensions. Here comes partial state into play. It allows you to call ",(0,r.yg)("inlineCode",{parentName:"p"},"getState")," method in ",(0,r.yg)("inlineCode",{parentName:"p"},"load")," method of an Extension. Received state consists of states collected from loaded Controller and Extensions loaded prior to the current Extension. Extensions are loaded in the same order as they were registered in a Controller."),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note"),": Promises in received state may not be resolved. Therefore you need to chain promises or use ",(0,r.yg)("inlineCode",{parentName:"p"},"async/await"),".")),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note"),": If you'll use ",(0,r.yg)("inlineCode",{parentName:"p"},"async/await")," execution will not be parallel relative to other promises.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/page/home/HomeController.js\nexport default class HomeController extends AbstractController {\n\n load() {\n const userPromise = this._userService.load(this.params.userId);\n\n return {\n user: userPromise\n };\n }\n}\n")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"// app/component/poll/PollExtension.js\nexport default class PollExtension extends AbstractExtension {\n getAllowedStateKeys() {\n return ['pollVotes'];\n }\n\n load() {\n const { user: userPromise } = this.getState();\n const pollVotesPromise = userPromise.then(\n user => this._pollService.getVotes(user.id)\n );\n\n return {\n pollVotes: pollVotesPromise\n };\n }\n}\n")),(0,r.yg)("h2",{id:"state-transactions"},"State transactions"),(0,r.yg)("p",null,"State transactions, similarly to SQL transactions, provide a way to queue state patches and then commit them as a one to the original state."),(0,r.yg)("p",null,"They're here for use cases where you'd in you workflow call ",(0,r.yg)("inlineCode",{parentName:"p"},"setState")," method multiple times or you'd have to collect state patches in a separate variable (this is hard to do across multiple methods)."),(0,r.yg)("p",null,"Transaction is initiated with ",(0,r.yg)("inlineCode",{parentName:"p"},"beginStateTransaction()")," in Controller/Extension. After that\nevery setState call is queued and doesn't change the state or re-render anything. If there\nis another transaction initiated before you commit you'll lost your patches."),(0,r.yg)("p",null,"If you want to see what changes are in queue from the begin of transaction call ",(0,r.yg)("inlineCode",{parentName:"p"},"getTransactionStatePatches()")," method."),(0,r.yg)("p",null,"To finish the transaction you have to call ",(0,r.yg)("inlineCode",{parentName:"p"},"commitStateTransaction()")," method. It will squash\nall the patches made during the transaction into a one and apply it to the original state.\nTherefore your application will re-render only once and you'll also receive ",(0,r.yg)("a",{parentName:"p",href:"./events#stateeventsbefore_change_state"},"state events")," only once."),(0,r.yg)("p",null,"Another way to finish the transaction is to cancel it via ",(0,r.yg)("inlineCode",{parentName:"p"},"cancelStateTransaction()")," method."),(0,r.yg)("blockquote",null,(0,r.yg)("p",{parentName:"blockquote"},(0,r.yg)("strong",{parentName:"p"},"Note"),": Call to ",(0,r.yg)("inlineCode",{parentName:"p"},"getState")," method after the transaction has begun will return state as it was before the transaction eg. the returned state doesn't include changes from the transaction period until the transaction is committed.")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"async onFormSubmit({ content, deleteRevisions = false }) {\n const { article } = this.getState();\n\n this.beginStateTransaction();\n\n const result = await this._http.put(/* ... */);\n\n if (deleteRevisions) {\n await this.deleteArticleRevisions();\n }\n\n this.setState({ article: Object.assign({}, article, {\xa0content }) });\n this.commitStateTransaction();\n}\n\nasync deleteArticleRevisions() {\n const { article, revisions } = this.getState();\n\n await this._http.delete(/* ... */);\n\n this.setState({ revisions: [] });\n}\n")),(0,r.yg)("p",null,"In the example above, after the form is submitted with ",(0,r.yg)("inlineCode",{parentName:"p"},"deleteRevisions = true"),":"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"Two ",(0,r.yg)("inlineCode",{parentName:"li"},"setState")," calls are made"),(0,r.yg)("li",{parentName:"ul"},"Only one render is triggered after the ",(0,r.yg)("inlineCode",{parentName:"li"},"commitStateTransaction")," call")))}g.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/33fd58a6.0c7e1569.js b/assets/js/33fd58a6.518886f0.js similarity index 99% rename from assets/js/33fd58a6.0c7e1569.js rename to assets/js/33fd58a6.518886f0.js index fd57bd413..f706464a3 100644 --- a/assets/js/33fd58a6.0c7e1569.js +++ b/assets/js/33fd58a6.518886f0.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_ima_docs=self.webpackChunk_ima_docs||[]).push([[633],{5680:(e,n,t)=>{t.d(n,{xA:()=>c,yg:()=>g});var a=t(6540);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function s(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var l=a.createContext({}),p=function(e){var n=a.useContext(l),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},c=function(e){var n=p(e.components);return a.createElement(l.Provider,{value:n},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},d=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,s=e.originalType,l=e.parentName,c=o(e,["components","mdxType","originalType","parentName"]),m=p(t),d=r,g=m["".concat(l,".").concat(d)]||m[d]||u[d]||s;return t?a.createElement(g,i(i({ref:n},c),{},{components:t})):a.createElement(g,i({ref:n},c))}));function g(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var s=t.length,i=new Array(s);i[0]=d;var o={};for(var l in n)hasOwnProperty.call(n,l)&&(o[l]=n[l]);o.originalType=e,o[m]="string"==typeof e?e:r,i[1]=o;for(var p=2;p{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>p});var a=t(8102),r=(t(6540),t(5680));const s={title:"Migration 0.15.0",description:"Migration > Migration to version 0.15.0"},i=void 0,o={unversionedId:"migration/migration-0.15.0",id:"migration/migration-0.15.0",title:"Migration 0.15.0",description:"Migration > Migration to version 0.15.0",source:"@site/../docs/migration/migration-0.15.0.md",sourceDirName:"migration",slug:"/migration/migration-0.15.0",permalink:"/migration/migration-0.15.0",draft:!1,editUrl:"https://github.com/seznam/ima/tree/master/docs/../docs/migration/migration-0.15.0.md",tags:[],version:"current",lastUpdatedBy:"lukasPan",lastUpdatedAt:1737723026,formattedLastUpdatedAt:"Jan 24, 2025",frontMatter:{title:"Migration 0.15.0",description:"Migration > Migration to version 0.15.0"},sidebar:"docs",previous:{title:"Migration 0.14.0",permalink:"/migration/migration-0.14.0"},next:{title:"Migration 0.16.0",permalink:"/migration/migration-0.16.0"}},l={},p=[{value:"Build",id:"build",level:2},{value:"Karma removed instead of that added Jest",id:"karma-removed-instead-of-that-added-jest",level:3},{value:"Server",id:"server",level:2},{value:"DocumentView",id:"documentview",level:2},{value:"SPA",id:"spa",level:2},{value:"Removed namespaces",id:"removed-namespaces",level:2},{value:"Others",id:"others",level:2}],c={toc:p},m="wrapper";function u(e){let{components:n,...t}=e;return(0,r.yg)(m,(0,a.A)({},c,t,{components:n,mdxType:"MDXLayout"}),(0,r.yg)("p",null,"In order to upgrade to IMA.js 0.15.0, start ba adding these new dependencies to package.json:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-json"},'"prop-types": "15.6.0",\n"react": "16.2.0",\n"react-dom": "16.2.0",\n"express-http-proxy": "^1.0.7"\n')),(0,r.yg)("h2",{id:"build"},"Build"),(0,r.yg)("p",null,"If you are overriding polyfills or shims (for example using some custom polyfills) you need to change polyfills or shims structure in gulpConfig.js . Now it has to be structure for js, es and fetch polyfills.\nIf you are't overriding polyfills or shims, you can skip this step."),(0,r.yg)("p",null,"Example:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"shim: {\n js: {\n name: 'shim.js',\n src: ['./node_modules/ima/polyfill/collectionEnumeration.js'],\n dest: {\n client: './build/static/js/'\n }\n },\n es: {\n name: 'shim.es.js',\n src: [],\n dest: {\n client: './build/static/js/',\n server: './build/ima/'\n }\n }\n\n\npolyfill: {\n js: {\n name: 'polyfill.js',\n src: [\n './node_modules/babel-polyfill/dist/polyfill.min.js',\n './node_modules/custom-event-polyfill/custom-event-polyfill.js'\n ],\n dest: {\n client: './build/static/js/'\n }\n },\n es: {\n name: 'polyfill.es.js',\n src: ['./node_modules/custom-event-polyfill/custom-event-polyfill.js'],\n dest: {\n client: './build/static/js/'\n }\n },\n fetch: {\n name: 'fetch-polyfill.js',\n src: [\n './node_modules/core-js/client/shim.min.js',\n './node_modules/whatwg-fetch/fetch.js'\n ],\n dest: {\n client: './build/static/js/'\n }\n },\n ima: {\n name: 'ima-polyfill.js',\n src: [\n './node_modules/ima/polyfill/imaLoader.js',\n './node_modules/ima/polyfill/imaRunner.js'\n ],\n dest: {\n client: './build/static/js/'\n }\n }\n }\n\n")),(0,r.yg)("p",null,"In build.js add new property 'es' to bundle object:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"es: [\n './build/static/js/polyfill.es.js',\n './build/static/js/shim.es.js',\n './build/static/js/vendor.client.es.js',\n './build/static/js/app.client.es.js'\n]\n")),(0,r.yg)("p",null,"Add to your settings.js ",(0,r.yg)("strong",{parentName:"p"},"prod"),".$Page.$Render new property esScripts like this:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"esScripts: [\n '/static/js/locale/' + config.$Language + '.js' + versionStamp,\n '/static/js/app.bundle.es.min.js' + versionStamp\n]\n")),(0,r.yg)("p",null,"Add to your settings.js ",(0,r.yg)("strong",{parentName:"p"},"dev"),".$Page.$Render new property esScripts like this:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"esScripts: [\n '/static/js/polyfill.es.js' + versionStamp,\n '/static/js/shim.es.js' + versionStamp,\n '/static/js/vendor.client.es.js' + versionStamp,\n `/static/js/locale/${config.$Language}.js${versionStamp}`,\n '/static/js/app.client.es.js' + versionStamp,\n '/static/js/hot.reload.js' + versionStamp\n]\n")),(0,r.yg)("h3",{id:"karma-removed-instead-of-that-added-jest"},"Karma removed instead of that added Jest"),(0,r.yg)("p",null,"If you are overriding gulpfile.js you need to make following changes:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"remove from gulpConfig.tasks.dev task ",(0,r.yg)("inlineCode",{parentName:"li"},"test:unit:karma:dev")),(0,r.yg)("li",{parentName:"ul"},"remove from gulpConfig.tasks.dev and gulpConfig.tasks.build task ",(0,r.yg)("inlineCode",{parentName:"li"},"Es6ToEs5:vendor:client:test")),(0,r.yg)("li",{parentName:"ul"},"remove from function buildExample task ",(0,r.yg)("inlineCode",{parentName:"li"},"Es6ToEs5:vendor:client:test"))),(0,r.yg)("p",null,"If you are overriding gulpConfig.tasks.build in gulpConfig.js you need to add ",(0,r.yg)("inlineCode",{parentName:"p"},"bundle:es:app")," into bundles section."),(0,r.yg)("h2",{id:"server"},"Server"),(0,r.yg)("p",null,"In server.js"),(0,r.yg)("p",null,"Add at the top into import sections:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"require('ima/polyfill/imaLoader.js');\nrequire('ima/polyfill/imaRunner.js');\n")),(0,r.yg)("p",null,"add proxy into middlewares imports section"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"let proxy = require('express-http-proxy');\n")),(0,r.yg)("p",null,"change line"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},".use(environment.$Proxy.path + '/', proxy)\n")),(0,r.yg)("p",null,"to"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},".use(environment.$Proxy.path + '/', proxy(environment.$Proxy.server))\n")),(0,r.yg)("h2",{id:"documentview"},"DocumentView"),(0,r.yg)("p",null,"In DocumentView.jsx we united sync and async scripts."),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"remove ",(0,r.yg)("inlineCode",{parentName:"li"},"getSyncScripts")," function."),(0,r.yg)("li",{parentName:"ul"},"update ",(0,r.yg)("inlineCode",{parentName:"li"},"getAsyncScripts")," function to")),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-javascript"},"getAsyncScripts() {\n let scriptResources = ` +is destroyed by the garbage collector.

+ \ No newline at end of file diff --git a/basic-features/data-fetching/index.html b/basic-features/data-fetching/index.html index 3d3a4e2d3..876fcc990 100644 --- a/basic-features/data-fetching/index.html +++ b/basic-features/data-fetching/index.html @@ -4,13 +4,13 @@ Data fetching | IMA.js - +
-

Data fetching

HttpAgent allows you to isomorphically fetch data in IMA.js applications. It is a simple wrapper around native fetch with additional features like caching, proxy support and others.

Cancellable requests

The HttpAgent has support for AbortController to cancel requests in native way. There are two ways you can provide custom instance of AbortController to the HttpAgent, where each has it's own benefits.

options.abortController

Using this approach has the added benefit of HttpAgent being able to additionally reuse this controller for cancelation of timeout requests.

const controller = new AbortController();

httpAgent.get('<uri>', '<data>', {
abortController: controller,
});

// Cancel the request
controller.abort();
info

If you don't provide custom instance of AbortController the agent uses it's own instance internally to cancel running timeout requests.

options.fetchOptions.signal

This approach is more similar to native fetch definition. However since currently you can only provide one signal to fetch request and we don't have access to the controller instance (from within the HttpAgent), we are unable to abort time out requests in this case.

const controller = new AbortController();

httpAgent.get('<uri>', '<data>', {
fetchOptions: {
signal: controller.signal,
}
});

// Cancel the request
controller.abort();
note

The time out requests still throw the same timeout error, however they are not canceled (aborted). This is the only difference between the two forementioned methods.

- +

Data fetching

HttpAgent allows you to isomorphically fetch data in IMA.js applications. It is a simple wrapper around native fetch with additional features like caching, proxy support and others.

Cancellable requests

The HttpAgent has support for AbortController to cancel requests in native way. There are two ways you can provide custom instance of AbortController to the HttpAgent, where each has it's own benefits.

options.abortController

Using this approach has the added benefit of HttpAgent being able to additionally reuse this controller for cancelation of timeout requests.

const controller = new AbortController();

httpAgent.get('<uri>', '<data>', {
abortController: controller,
});

// Cancel the request
controller.abort();
info

If you don't provide custom instance of AbortController the agent uses it's own instance internally to cancel running timeout requests.

options.fetchOptions.signal

This approach is more similar to native fetch definition. However since currently you can only provide one signal to fetch request and we don't have access to the controller instance (from within the HttpAgent), we are unable to abort time out requests in this case.

const controller = new AbortController();

httpAgent.get('<uri>', '<data>', {
fetchOptions: {
signal: controller.signal,
}
});

// Cancel the request
controller.abort();
note

The time out requests still throw the same timeout error, however they are not canceled (aborted). This is the only difference between the two forementioned methods.

+ \ No newline at end of file diff --git a/basic-features/dictionary/index.html b/basic-features/dictionary/index.html index 1ef9150b0..ae79425fa 100644 --- a/basic-features/dictionary/index.html +++ b/basic-features/dictionary/index.html @@ -4,13 +4,13 @@ Dictionary | IMA.js - +
-

Dictionary

Dictionary in IMA.js app serves many purposes. Simplest of them is keeping text strings out of component markup. More advanced one would be internationalization and in-text replacements.

Configuration

First we need to tell IMA.js where to look for dictionary files. Naming convention of the files is up to you, but it should be clear what language are the files meant for and glob pattern has to be able to match path to the files. IMA.js defaults to the following configuration:

languages: {
cs: ['./app/**/*CS.json'],
en: ['./app/**/*EN.json']
}

However you can easily override this settings in ima.config.js (an example):

./ima.config.js
module.exports = {
languages: {
cs: [
'./app/component/**/*CS.json',
'./app/page/**/*CS.json'
],
en: [
'./app/component/**/*EN.json',
'./app/page/**/*EN.json'
],
de: [
'./app/component/**/*DE.json',
'./app/page/**/*DE.json'
]
}
}

URL parser configuration

We also need to specify what language should be loaded. This is done dynamically depending on current URL. You can customize the URL patterns to language mapping in environment settings.

The configuration consists of simple key-value pairs, that are used for configuring the languages used with specific hosts or starting paths:

  • key - has to start with '//' instead of a protocol, and you can define the root path.
  • value - is a language to use when the key is matched by the current URL.
./server/config/environment.js
module.exports = (() => ({
prod: {
$Language: {
'//*:*/cs': 'cs', // https://ima-app.com/cs/custom-route
'//*:*/en': 'en', // https://ima-app.com/en/custom-route
'//*:*': 'cs', // https://ima-app.com/custom-route
},
}
}))();

:language placeholder

To make the language definition a bit easier for multilingua applications, you can use :language placeholder in following way:

./server/config/environment.js
module.exports = (() => ({
prod: {
$Language: {
'//*:*/:language': ':language', // https://ima-app.com/[en|cs]/custom-route
'//*:*': 'cs', // https://ima-app.com/custom-route
},
}
}))();

Language files

The messageformat compiler, which processes our language files, expects .JSON files on the input. Contents of these files are objects, which can be nested into multiple levels. These levels are then represtend as a namespace key to the value in the dictionary.

./pollVoteEN.json
{
"resultTitle": "Result of {name}:",
"result": {
"voted": "{count, plural, =0{Found no results} one{Found one result} other{Found # results} }",
"reader": "{gender, select, male{He said} female{She said} other{They said} }",
}
}
info

File name is used as a namespace for strings it defines. String defined under key submit in file uploadFormCS.json will be accessible under uploadForm.submit.

Usage

Every component and view extending AbstractComponent or AbstractPureComponent has access to localize method from within its instance. This method is alias to a get method from the Dictionary instance and takes 2 arguments:

  • key - namespace and name of the localization string -> if you have resultTitle string in file pollVoteEN.json the key to this string would be pollVote.resultTitle.
  • parameters - Optional object with replacements and parameters for messageformat syntax. For more info about the syntax check out ICU guide.
import { AbstractPureComponent } from '@ima/react-page-renderer';

class PollVote extends AbstractPureComponent {
render() {
return (
<div>
{this.localize('pollVote.resultTitle')}
{this.localize('pollVote.result.voted', {count: 3})}
</div>
);
}
}
tip

Use useComponent().localize or useLocalize() hooks in functional components.

import { useComponent, useLocalize } from '@ima/react-page-renderer';

function PollVote() {
// const { localize } = useComponent();
const localize = useLocalize();

return (
<div>
{localize('pollVote.resultTitle')}
{localize('pollVote.result.voted', {count: 3})}
</div>
);
}

Messageformat library

For more information on the available selectors, formatters, and other details, please see Format guide.

Dictionary is also registered in Object Container and thus can be obtained in Controllers, Extensions and other classes constructed through OC.

- +

Dictionary

Dictionary in IMA.js app serves many purposes. Simplest of them is keeping text strings out of component markup. More advanced one would be internationalization and in-text replacements.

Configuration

First we need to tell IMA.js where to look for dictionary files. Naming convention of the files is up to you, but it should be clear what language are the files meant for and glob pattern has to be able to match path to the files. IMA.js defaults to the following configuration:

languages: {
cs: ['./app/**/*CS.json'],
en: ['./app/**/*EN.json']
}

However you can easily override this settings in ima.config.js (an example):

./ima.config.js
module.exports = {
languages: {
cs: [
'./app/component/**/*CS.json',
'./app/page/**/*CS.json'
],
en: [
'./app/component/**/*EN.json',
'./app/page/**/*EN.json'
],
de: [
'./app/component/**/*DE.json',
'./app/page/**/*DE.json'
]
}
}

URL parser configuration

We also need to specify what language should be loaded. This is done dynamically depending on current URL. You can customize the URL patterns to language mapping in environment settings.

The configuration consists of simple key-value pairs, that are used for configuring the languages used with specific hosts or starting paths:

  • key - has to start with '//' instead of a protocol, and you can define the root path.
  • value - is a language to use when the key is matched by the current URL.
./server/config/environment.js
module.exports = (() => ({
prod: {
$Language: {
'//*:*/cs': 'cs', // https://ima-app.com/cs/custom-route
'//*:*/en': 'en', // https://ima-app.com/en/custom-route
'//*:*': 'cs', // https://ima-app.com/custom-route
},
}
}))();

:language placeholder

To make the language definition a bit easier for multilingua applications, you can use :language placeholder in following way:

./server/config/environment.js
module.exports = (() => ({
prod: {
$Language: {
'//*:*/:language': ':language', // https://ima-app.com/[en|cs]/custom-route
'//*:*': 'cs', // https://ima-app.com/custom-route
},
}
}))();

Language files

The messageformat compiler, which processes our language files, expects .JSON files on the input. Contents of these files are objects, which can be nested into multiple levels. These levels are then represtend as a namespace key to the value in the dictionary.

./pollVoteEN.json
{
"resultTitle": "Result of {name}:",
"result": {
"voted": "{count, plural, =0{Found no results} one{Found one result} other{Found # results} }",
"reader": "{gender, select, male{He said} female{She said} other{They said} }",
}
}
info

File name is used as a namespace for strings it defines. String defined under key submit in file uploadFormCS.json will be accessible under uploadForm.submit.

Usage

Every component and view extending AbstractComponent or AbstractPureComponent has access to localize method from within its instance. This method is alias to a get method from the Dictionary instance and takes 2 arguments:

  • key - namespace and name of the localization string -> if you have resultTitle string in file pollVoteEN.json the key to this string would be pollVote.resultTitle.
  • parameters - Optional object with replacements and parameters for messageformat syntax. For more info about the syntax check out ICU guide.
import { AbstractPureComponent } from '@ima/react-page-renderer';

class PollVote extends AbstractPureComponent {
render() {
return (
<div>
{this.localize('pollVote.resultTitle')}
{this.localize('pollVote.result.voted', {count: 3})}
</div>
);
}
}
tip

Use useComponent().localize or useLocalize() hooks in functional components.

import { useComponent, useLocalize } from '@ima/react-page-renderer';

function PollVote() {
// const { localize } = useComponent();
const localize = useLocalize();

return (
<div>
{localize('pollVote.resultTitle')}
{localize('pollVote.result.voted', {count: 3})}
</div>
);
}

Messageformat library

For more information on the available selectors, formatters, and other details, please see Format guide.

Dictionary is also registered in Object Container and thus can be obtained in Controllers, Extensions and other classes constructed through OC.

+ \ No newline at end of file diff --git a/basic-features/error-handling/index.html b/basic-features/error-handling/index.html index 1da1125fa..5fbe31dc4 100644 --- a/basic-features/error-handling/index.html +++ b/basic-features/error-handling/index.html @@ -4,13 +4,13 @@ Error Handling | IMA.js - +
-

Error Handling

This sections focuses on client and server-side error handling during development and in production.

GenericError

Represents custom error class that poses a structure for http errors. This should be preferred way of throwing custom errors as it adds an ability to define http status code with additional custom params. To create such error you need to import GenericError from @ima/core and instantiate it:

import { GenericError } from '@ima/core';

throw new GenericError(
'Something went wrong.',
{ status: 500, custom: 'param' } // error parameters
);

GenericError instance has 2 methods:

  • getParams() - Returns params argument (2nd argument) provided to the constructor.
  • getHttpStatus() - Returns status property from the params.
- +

Error Handling

This sections focuses on client and server-side error handling during development and in production.

GenericError

Represents custom error class that poses a structure for http errors. This should be preferred way of throwing custom errors as it adds an ability to define http status code with additional custom params. To create such error you need to import GenericError from @ima/core and instantiate it:

import { GenericError } from '@ima/core';

throw new GenericError(
'Something went wrong.',
{ status: 500, custom: 'param' } // error parameters
);

GenericError instance has 2 methods:

  • getParams() - Returns params argument (2nd argument) provided to the constructor.
  • getHttpStatus() - Returns status property from the params.
+ \ No newline at end of file diff --git a/basic-features/events/index.html b/basic-features/events/index.html index 3078bba09..ae2c65de2 100644 --- a/basic-features/events/index.html +++ b/basic-features/events/index.html @@ -4,7 +4,7 @@ Events | IMA.js - + @@ -51,8 +51,8 @@ loaded be the new Controller are resolved.

StateEvents.BEFORE_CHANGE_STATE

An event fired before the page state changes. The handler of this event receives following data:

{
// The state object derived from the oldState and patchState
newState,
// The current state
oldState,
// The data that were passed to the `setState` method
patchState
}

Note: You can mutate the newState object if you wish. Mutating oldState and patchState will have no effect.

StateEvents.AFTER_CHANGE_STATE

An event fired after the page state changes. The data passed with this event -contain only the newState object.

RendererEvents.MOUNTED

PageRenderer fires this event after current page view is mounted to the DOM. Event's data contain { type: String } Where type can be one of constants located in @ima/core/page/renderer/Types.

RendererEvents.UPDATED

PageRenderer fires this event after current state is updated in the DOM. Event's data contain { state: Object<string, *>}.

RendererEvents.UNMOUNTED

PageRenderer fires this event after current view is unmounted from the DOM. Event's data contain { type: String } Where type can be one of constants located in @ima/core/page/renderer/Types.

RendererEvents.ERROR

PageRenderer fires this event when there is no _viewContainer in _renderToDOM method. Event's data contain { message: string }.

- +contain only the newState object.

RendererEvents.MOUNTED

PageRenderer fires this event after current page view is mounted to the DOM. Event's data contain { type: String } Where type can be one of constants located in @ima/core/page/renderer/Types.

RendererEvents.UPDATED

PageRenderer fires this event after current state is updated in the DOM. Event's data contain { state: Object<string, *>}.

RendererEvents.UNMOUNTED

PageRenderer fires this event after current view is unmounted from the DOM. Event's data contain { type: String } Where type can be one of constants located in @ima/core/page/renderer/Types.

RendererEvents.ERROR

PageRenderer fires this event when there is no _viewContainer in _renderToDOM method. Event's data contain { message: string }.

+ \ No newline at end of file diff --git a/basic-features/extensions/index.html b/basic-features/extensions/index.html index 2fbfe2747..408d198c4 100644 --- a/basic-features/extensions/index.html +++ b/basic-features/extensions/index.html @@ -4,7 +4,7 @@ Extensions | IMA.js - + @@ -27,8 +27,8 @@ ima/extension/AbstractExtension with the same methods as you'd use in the controller. In addition you should implement getAllowedStateKeys() method which returns array of keys the extension is allowed to change in controller's state. (If the extension itself creates the state key, it does not have to be claimed this way - the extension has access to it automatically.)

Note: List and description of controller methods can be seen in Controller lifecycle.

// app/component/gallery/GalleryExtension.js
import { AbstractExtension } from '@ima/core';

export default class GalleryExtension extends AbstractExtension {
static get $dependencies() {
return [];
}

load() {
// Where the magic happens...
}
}

All extensions to be used on a page must be added to the current controller via $extensions getter. After that, the extensions will go through the same lifecycle as the controller.

import { AbstractController } from '@ima/core';
import GalleryExtension from 'app/component/gallery/GalleryExtension';

export default class PostController extends AbstractController {
static get $dependencies() {
return [];
}

static get $extensions() {
return [GalleryExtension];
}

constructor() {
//If needed, extension instance can be retrieved using getExtension();
this._galleryExtension = this.getExtension(GalleryExtension);
}

...
}

Extensions can be also defined within routes.

//routes.js

import PostController from 'app/page/post/PostController';
import PostView from 'app/page/post/PostView';
import GalleryExtension from 'app/component/gallery/GalleryExtension';

export default (ns, oc, routesConfig, router) =>
router
.add('home', '/', PostController, PostView, {
extensions: [GalleryExtension],
});

Extensions can be defined in global array of extensions. -This array can be registered in the OC as constant:


//bind.js
import Extension1 from 'app/component/gallery/GalleryExtension';
import Extension2 from 'app/component/gallery/GalleryExtension2';
...
export default (ns, oc, config) => {
oc.constant('$galleryExtensions', [Extension, Extension2]);
}

//PostController.js
import { AbstractController } from '@ima/core';
import GalleryExtension from 'app/component/gallery/GalleryExtension';

export default class PostController extends AbstractController {

...

static get $extensions() {
return ['...$galleryExtensions'];
}

...
}

//Or in routes.js
...
router
.add('home', '/', PostController, PostView, {
extensions: ['...$galleryExtensions'],
});
...

Or just exported array:


//GalleryExtensions.js
import Extension1 from 'app/component/gallery/GalleryExtension';
import Extension2 from 'app/component/gallery/GalleryExtension2';

export const GalleryExtensions = [Extension, Extension2];


//PostController.js
import { AbstractController } from '@ima/core';
import { GalleryExtensions } from 'app/component/gallery/GalleryExtensions';

export default class PostController extends AbstractController {

...

static get $extensions() {
return [...GalleryExtensions];
}

...
}

Passing partial state from controllers

During any lifecycle phase of the page the controller's lifecycle method is called first and then the same method is called on every extension registered in the controller. Order of the extensions is crucial and the same as in which the extensions were registered.

Since v16 you can access the state loaded in controller and preceding extensions (hence the cruciality of extensions order). Bear in mind that the accessed state may contain unresolved promises that need to be treated differently.

Addition of async/await functionality in v17 can lead to dramatic performance drop if not used well. Keep in mind that every await in Controller's or Extension's load method will delay execution of next load method until the asynchronous operation finishes.

// app/component/gallery/GalleryExtension.js

load() {
const {
user: userPromise, // needs to be chained with .then()
userGroupName,
} = this.getState();

const galleryPromise = userPromise.then(user => {
// Calling this.getState() here would still give us unresolved promises.
return this._galleryService.loadByUserName(user.name);
});

return {
gallery: galleryPromise
};
}
- +This array can be registered in the OC as constant:


//bind.js
import Extension1 from 'app/component/gallery/GalleryExtension';
import Extension2 from 'app/component/gallery/GalleryExtension2';
...
export default (ns, oc, config) => {
oc.constant('$galleryExtensions', [Extension, Extension2]);
}

//PostController.js
import { AbstractController } from '@ima/core';
import GalleryExtension from 'app/component/gallery/GalleryExtension';

export default class PostController extends AbstractController {

...

static get $extensions() {
return ['...$galleryExtensions'];
}

...
}

//Or in routes.js
...
router
.add('home', '/', PostController, PostView, {
extensions: ['...$galleryExtensions'],
});
...

Or just exported array:


//GalleryExtensions.js
import Extension1 from 'app/component/gallery/GalleryExtension';
import Extension2 from 'app/component/gallery/GalleryExtension2';

export const GalleryExtensions = [Extension, Extension2];


//PostController.js
import { AbstractController } from '@ima/core';
import { GalleryExtensions } from 'app/component/gallery/GalleryExtensions';

export default class PostController extends AbstractController {

...

static get $extensions() {
return [...GalleryExtensions];
}

...
}

Passing partial state from controllers

During any lifecycle phase of the page the controller's lifecycle method is called first and then the same method is called on every extension registered in the controller. Order of the extensions is crucial and the same as in which the extensions were registered.

Since v16 you can access the state loaded in controller and preceding extensions (hence the cruciality of extensions order). Bear in mind that the accessed state may contain unresolved promises that need to be treated differently.

Addition of async/await functionality in v17 can lead to dramatic performance drop if not used well. Keep in mind that every await in Controller's or Extension's load method will delay execution of next load method until the asynchronous operation finishes.

// app/component/gallery/GalleryExtension.js

load() {
const {
user: userPromise, // needs to be chained with .then()
userGroupName,
} = this.getState();

const galleryPromise = userPromise.then(user => {
// Calling this.getState() here would still give us unresolved promises.
return this._galleryService.loadByUserName(user.name);
});

return {
gallery: galleryPromise
};
}
+ \ No newline at end of file diff --git a/basic-features/handling-scripts-and-styles/index.html b/basic-features/handling-scripts-and-styles/index.html index e8a427430..ae181f4fb 100644 --- a/basic-features/handling-scripts-and-styles/index.html +++ b/basic-features/handling-scripts-and-styles/index.html @@ -4,13 +4,13 @@ Handling scripts and styles | IMA.js - + - +
+ \ No newline at end of file diff --git a/basic-features/object-container/index.html b/basic-features/object-container/index.html index 1ff148bc4..c46ac7ca5 100644 --- a/basic-features/object-container/index.html +++ b/basic-features/object-container/index.html @@ -4,7 +4,7 @@ Object Container | IMA.js - + @@ -37,8 +37,8 @@ factory function has been registered with the object container if no custom dependencies are provided.

import { Cache, HttpAgent } from '@ima/core';
import SimpleRestClient from 'app/rest-client-impl/SimpleRestClient';
import LinkGenerator from 'app/rest-client-impl/LinkGenerator';

oc.create('UserAgent');
oc.create(
SimpleRestClient,
[
HttpAgent, Cache, 'REST_API_ROOT_URL', LinkGenerator
]
);

The last two method are not used as much as the first one but can be useful inside the app/config/bind.js and app/config/routes.js

Other methods

  • has() returns true if the specified object, class or resource is registered -within the OC.
if (oc.has('UserAgent') && oc.get('UserAgent').isMobile()) {
// Register conditional stuff here...
}
  • getConstructorOf() returns the class constructor function of the specified class or alias.
- +within the OC.
if (oc.has('UserAgent') && oc.get('UserAgent').isMobile()) {
// Register conditional stuff here...
}
  • getConstructorOf() returns the class constructor function of the specified class or alias.
+ \ No newline at end of file diff --git a/basic-features/page-manager/index.html b/basic-features/page-manager/index.html index ef33880cd..7f55860ec 100644 --- a/basic-features/page-manager/index.html +++ b/basic-features/page-manager/index.html @@ -4,15 +4,15 @@ Page Manager | IMA.js - +

Page Manager

Page Manager is an essential part of IMA.js. It's something like a puppeteer that manipulates with pages and views. Once a router matches URL to one of route's path the page manager takes care of the rest.

Managing process

If the new matched route has onlyUpdate option set to true and the controller and view hasn't changed the route transition is dispatched only through update method of the controller.

In every other case the manager goes through it's full process:

  1. Unload previous controller and extensions - To make room for the new, manager has to get rid of the old controller and extensions. First calls deactivate method on every extension registered in the old controller and then the same method on the controller itself. Same process follows with destroy method.

  2. Clear state and unmount view - After unloading controller and extensions the page state is cleared and view (starting from ManagedRootView) is unmounted. However if the DocumentView, ViewAdapter and ManagedRootView are the same for the new route the view is cleared rather then unmounted. This way you can achieve component persistency.

  3. Loading new controller and extensions - After the manager is done with clearing previous resource it initializes the new ones. First the init method is called on controller then on every extension (Extensions may be initialized during the controllers init method call). -When the initialization is complete manager starts loading resources via load method of the controller and extensions. For detailed explanation see the load method documentation.

  4. Rendering new view - After the load method has been called a view for the controller is rendered. It doesn't matter if all promises returned by the load method have been resolved. The process of handling promises is described in the load method documentation. Following rendering process is described on a page Rendering process and View & Components.

Intervene into the process

It's possible for you to intervene into the process before it starts and after it finished. One way is to listen to BEFORE_HANDLE_ROUTE and AFTER_HANDLE_ROUTE dispatcher events. However from inside event listeners you cannot intercept or modify the process. For this purpose we've introduced PageManagerHandlers in v16

PageManagerHandlers

PageManagerHandler is a simple class that extends ima/page/handler/PageHandler. It can obtain dependencies through dependency injection. Each handler should contain 4 methods:

1. init() method

For purpose of initializing.

2. handlePreManagedState() method

This method is called before the page manager start taking any action. It receives 3 arguments managedPage, nextManagedPage and action. managedPage holds information about current page, nextManagedPage about following page. Each of the "managed page" arguments has following shape:

{
controller: ?(string|function(new: Controller)), // controller class
controllerInstance: ?Controller, // instantiated controller
decoratedController: ?Controller, // controller decorator created from controller instance
view: ?React.Component, // view class/component
viewInstance: ?React.Element, // instantiated view
route: ?Route, // matched route that leads to the controller
options: ?RouteOptions, // route options
params: ?Object<string, string>, // route parameters and their values
state: {
activated: boolean // if the page has been activated
}
}

and finally the action is an object describing what triggered the routing. If a PopStateEvent triggered the routing the action object will look like this: { type: 'popstate', event: PopStateEvent } otherwise the event property will contain MouseEvent (e.g. clicked on a link) and type property will have value 'redirect', 'click' or 'error'.

3. handlePostManagedState() method

This method is a counterpart to handlePreManagedState() method. It's called after page transition is finished. It receives similar arguments (managedPage, previousManagedPage and action). previousManagedPage holds information about previous page.

Note: handlePreManagedState() and handlePostManagedState() methods can interrupt transition process by throwing an error. The thrown error should be instance of GenericError with a status code specified. That way the router can handle thrown error accordingly.

4. destroy() method

For purpose of destructing

Registering PageManagerHandlers

PageManagerHandlers have their own registry PageHandlerRegistry. Every handler you create should be registered as a dependency of this registry.

// app/config/bind.js
import { PageHandlerRegistry, Window } from '@ima/core';
import MyOwnHandler from 'app/handler/MyOwnHandler';

export let init = (ns, oc, config) => {
// ...

if (oc.get(Window).isClient()) { // register different handlers for client and server
oc.inject(PageHandlerRegistry, [MyOwnHandler]);
} else {
oc.inject(PageHandlerRegistry, []);
}
};

Note: Handlers are executed in series and each one waits for the previous one to complete its task.

With introduction of PageManagerHandlers in v16 we've moved some functionality to predefined handler PageNavigationHandler. This handler takes care of saving scroll position, restoring scroll position and settings browser's address bar URL. You're free to extend it, override it or whatever else you want.

PageNavigationHandler is registered by default, but when you register your own handlers you need to specify PageNavigationHandler as well.

import { PageHandlerRegistry, PageNavigationHandler } from '@ima/core';
import MyOwnHandler from 'app/handler/MyOwnHandler';

export let init = (ns, oc, config) => {
// ...
oc.inject(PageHandlerRegistry, [PageNavigationHandler, MyOwnHandler]);
};
- +When the initialization is complete manager starts loading resources via load method of the controller and extensions. For detailed explanation see the load method documentation.

  • Rendering new view - After the load method has been called a view for the controller is rendered. It doesn't matter if all promises returned by the load method have been resolved. The process of handling promises is described in the load method documentation. Following rendering process is described on a page Rendering process and View & Components.

  • Intervene into the process

    It's possible for you to intervene into the process before it starts and after it finished. One way is to listen to BEFORE_HANDLE_ROUTE and AFTER_HANDLE_ROUTE dispatcher events. However from inside event listeners you cannot intercept or modify the process. For this purpose we've introduced PageManagerHandlers in v16

    PageManagerHandlers

    PageManagerHandler is a simple class that extends ima/page/handler/PageHandler. It can obtain dependencies through dependency injection. Each handler should contain 4 methods:

    1. init() method

    For purpose of initializing.

    2. handlePreManagedState() method

    This method is called before the page manager start taking any action. It receives 3 arguments managedPage, nextManagedPage and action. managedPage holds information about current page, nextManagedPage about following page. Each of the "managed page" arguments has following shape:

    {
    controller: ?(string|function(new: Controller)), // controller class
    controllerInstance: ?Controller, // instantiated controller
    decoratedController: ?Controller, // controller decorator created from controller instance
    view: ?React.Component, // view class/component
    viewInstance: ?React.Element, // instantiated view
    route: ?Route, // matched route that leads to the controller
    options: ?RouteOptions, // route options
    params: ?Object<string, string>, // route parameters and their values
    state: {
    activated: boolean // if the page has been activated
    }
    }

    and finally the action is an object describing what triggered the routing. If a PopStateEvent triggered the routing the action object will look like this: { type: 'popstate', event: PopStateEvent } otherwise the event property will contain MouseEvent (e.g. clicked on a link) and type property will have value 'redirect', 'click' or 'error'.

    3. handlePostManagedState() method

    This method is a counterpart to handlePreManagedState() method. It's called after page transition is finished. It receives similar arguments (managedPage, previousManagedPage and action). previousManagedPage holds information about previous page.

    Note: handlePreManagedState() and handlePostManagedState() methods can interrupt transition process by throwing an error. The thrown error should be instance of GenericError with a status code specified. That way the router can handle thrown error accordingly.

    4. destroy() method

    For purpose of destructing

    Registering PageManagerHandlers

    PageManagerHandlers have their own registry PageHandlerRegistry. Every handler you create should be registered as a dependency of this registry.

    // app/config/bind.js
    import { PageHandlerRegistry, Window } from '@ima/core';
    import MyOwnHandler from 'app/handler/MyOwnHandler';

    export let init = (ns, oc, config) => {
    // ...

    if (oc.get(Window).isClient()) { // register different handlers for client and server
    oc.inject(PageHandlerRegistry, [MyOwnHandler]);
    } else {
    oc.inject(PageHandlerRegistry, []);
    }
    };

    Note: Handlers are executed in series and each one waits for the previous one to complete its task.

    With introduction of PageManagerHandlers in v16 we've moved some functionality to predefined handler PageNavigationHandler. This handler takes care of saving scroll position, restoring scroll position and settings browser's address bar URL. You're free to extend it, override it or whatever else you want.

    PageNavigationHandler is registered by default, but when you register your own handlers you need to specify PageNavigationHandler as well.

    import { PageHandlerRegistry, PageNavigationHandler } from '@ima/core';
    import MyOwnHandler from 'app/handler/MyOwnHandler';

    export let init = (ns, oc, config) => {
    // ...
    oc.inject(PageHandlerRegistry, [PageNavigationHandler, MyOwnHandler]);
    };
    + \ No newline at end of file diff --git a/basic-features/page-state/index.html b/basic-features/page-state/index.html index 458331dbf..870fb26c5 100644 --- a/basic-features/page-state/index.html +++ b/basic-features/page-state/index.html @@ -4,7 +4,7 @@ Page State | IMA.js - + @@ -14,8 +14,8 @@ every setState call is queued and doesn't change the state or re-render anything. If there is another transaction initiated before you commit you'll lost your patches.

    If you want to see what changes are in queue from the begin of transaction call getTransactionStatePatches() method.

    To finish the transaction you have to call commitStateTransaction() method. It will squash all the patches made during the transaction into a one and apply it to the original state. -Therefore your application will re-render only once and you'll also receive state events only once.

    Another way to finish the transaction is to cancel it via cancelStateTransaction() method.

    Note: Call to getState method after the transaction has begun will return state as it was before the transaction eg. the returned state doesn't include changes from the transaction period until the transaction is committed.

    async onFormSubmit({ content, deleteRevisions = false }) {
    const { article } = this.getState();

    this.beginStateTransaction();

    const result = await this._http.put(/* ... */);

    if (deleteRevisions) {
    await this.deleteArticleRevisions();
    }

    this.setState({ article: Object.assign({}, article, { content }) });
    this.commitStateTransaction();
    }

    async deleteArticleRevisions() {
    const { article, revisions } = this.getState();

    await this._http.delete(/* ... */);

    this.setState({ revisions: [] });
    }

    In the example above, after the form is submitted with deleteRevisions = true:

    • Two setState calls are made
    • Only one render is triggered after the commitStateTransaction call
    - +Therefore your application will re-render only once and you'll also receive state events only once.

    Another way to finish the transaction is to cancel it via cancelStateTransaction() method.

    Note: Call to getState method after the transaction has begun will return state as it was before the transaction eg. the returned state doesn't include changes from the transaction period until the transaction is committed.

    async onFormSubmit({ content, deleteRevisions = false }) {
    const { article } = this.getState();

    this.beginStateTransaction();

    const result = await this._http.put(/* ... */);

    if (deleteRevisions) {
    await this.deleteArticleRevisions();
    }

    this.setState({ article: Object.assign({}, article, { content }) });
    this.commitStateTransaction();
    }

    async deleteArticleRevisions() {
    const { article, revisions } = this.getState();

    await this._http.delete(/* ... */);

    this.setState({ revisions: [] });
    }

    In the example above, after the form is submitted with deleteRevisions = true:

    • Two setState calls are made
    • Only one render is triggered after the commitStateTransaction call
    + \ No newline at end of file diff --git a/basic-features/rendering-process/index.html b/basic-features/rendering-process/index.html index 0e123fdec..d7bdfb16b 100644 --- a/basic-features/rendering-process/index.html +++ b/basic-features/rendering-process/index.html @@ -4,7 +4,7 @@ Rendering process | IMA.js - + @@ -49,8 +49,8 @@ https://www.kiwi.com/en/search/, https://airbnb.com/).

    // app/page/MapManagedRootView.js

    import { BlankManagedRootView } from '@ima/core';
    import PropTypes from 'prop-types';
    import React from 'react';
    import Map from 'app/component/map/Map';
    import MapResult from 'app/component/map/MapResult';

    export default class MapManagedRootView extends BlankManagedRootView {

    // ...

    render() {
    // Obtain search results and map settings from page state.
    const { searchResults, mapType } = this.props;

    return (
    <React.Fragment>
    {super.render()}
    <Map
    type = { mapType }
    centerOnResults = { true }>
    { searchResults.map(result => (
    <MapResult place = { result }/>
    ))}
    </Map>
    </React.Fragment>
    );
    }
    }

    Then the MapManagedRootView can be used in app/config/setting.js (property managedRootView) or in route options the same way as DocumentView or ViewAdapter.

    As you may have notices MapManagedRootView extends BlankManagedRootView which is also the default ManagedRootView when you don't specify your own. render() method of BlankManagedRootView simply renders View for current route with props containing current page state.

    Now when you know how a big part of the rendering process goes it's time to -have a look subsequent View and Component rendering.

    - +have a look subsequent View and Component rendering.

    + \ No newline at end of file diff --git a/basic-features/routing/async-routing/index.html b/basic-features/routing/async-routing/index.html index 7fffa6dda..4c80cd557 100644 --- a/basic-features/routing/async-routing/index.html +++ b/basic-features/routing/async-routing/index.html @@ -4,13 +4,13 @@ Async Routing | IMA.js - +
    -

    Async Routing

    Async routing allows you to split views and controllers into separate bundles and load them dynamically. This can be useful for some specific routes, that are not visited regularly and contain large amounts of unique code.

    To take advantage of this feature, you simply wrap your controller and view arguments into async function which calls a dynamic import():

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add(
    'home',
    '/',
    async() => import('app/page/home/HomeController'),
    async() => import('app/page/home/HomeView')
    )
    }
    tip

    When using default exports, you don't have to explicitly set the import promise to the default export, the router does this by default.

    However when using named exports you need to let the router know, where is the controller/view located in the resolved promise:

    async() => import('app/page/home/HomeView').then(module => module.HomeView);

    Merging view and controller imports into one

    Since the method above produces 2 separate JS chunk files (can depend on the actual environment). If you have really small controller and view files, you can help webpack in creating only one small chunk file which usually loads faster.

    This can be done by exporting view and controller from the same file:

    ./app/page/home/index.js
    export { default as HomeView } from './HomeView';
    export { default as HomeController } from './HomeController';

    And then merging those two dynamic imports into one:

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    const homeModules = async () => import('app/page/home');

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add(
    'home',
    '/',
    async () => homeModules().then(module => module.HomeController),
    async () => homeModules().then(module => module.HomeView)
    )
    }

    Preloading routeHandlers

    Each route handler exposes preload() method, which can be used to programmatically trigger preload of the dynamic imports for specific route.

    tip

    Use this in situations when the browser is idle and you want to preload some specific route handlers that the user will probably go next. This speeds up the responsiveness of your application dramatically.

    To call the preload() method, you first need to get access to the Router instance (we can use useComponentUtils hook in this example) and then you can use getRouteHandler() method to get specific route handler instance. After that just call preload() on this handler:

    ./app/config/routes.js
    import { useComponentUtils } from '@ima/react-hooks';

    export default function Card() {
    const { $Router } = useComponentUtils();
    const homeRouteHandler = $Router.getRouteHandler('home');

    useEffect(() => {
    homeRouteHandler.preload();
    }, [])

    return (
    <a href={$Router.link('home')}>Home</a>
    );
    }

    The method returns a promise, which resolves to tuple of [controller, view] instances.

    Prefetching/Preloading modules

    As with the dynamic imports, you can also use webpack directives for prefetching and preloading. Simply use the inline commend as it is mentioned in the webpack documentation.

    ./app/config/routes.js
    // ...
    async() => import(/* webpackPrefetch: true */ 'app/page/home/HomeController'),
    async() => import(/* webpackPreload: true */ 'app/page/home/HomeView')
    // ...
    - +

    Async Routing

    Async routing allows you to split views and controllers into separate bundles and load them dynamically. This can be useful for some specific routes, that are not visited regularly and contain large amounts of unique code.

    To take advantage of this feature, you simply wrap your controller and view arguments into async function which calls a dynamic import():

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add(
    'home',
    '/',
    async() => import('app/page/home/HomeController'),
    async() => import('app/page/home/HomeView')
    )
    }
    tip

    When using default exports, you don't have to explicitly set the import promise to the default export, the router does this by default.

    However when using named exports you need to let the router know, where is the controller/view located in the resolved promise:

    async() => import('app/page/home/HomeView').then(module => module.HomeView);

    Merging view and controller imports into one

    Since the method above produces 2 separate JS chunk files (can depend on the actual environment). If you have really small controller and view files, you can help webpack in creating only one small chunk file which usually loads faster.

    This can be done by exporting view and controller from the same file:

    ./app/page/home/index.js
    export { default as HomeView } from './HomeView';
    export { default as HomeController } from './HomeController';

    And then merging those two dynamic imports into one:

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    const homeModules = async () => import('app/page/home');

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add(
    'home',
    '/',
    async () => homeModules().then(module => module.HomeController),
    async () => homeModules().then(module => module.HomeView)
    )
    }

    Preloading routeHandlers

    Each route handler exposes preload() method, which can be used to programmatically trigger preload of the dynamic imports for specific route.

    tip

    Use this in situations when the browser is idle and you want to preload some specific route handlers that the user will probably go next. This speeds up the responsiveness of your application dramatically.

    To call the preload() method, you first need to get access to the Router instance (we can use useComponentUtils hook in this example) and then you can use getRouteHandler() method to get specific route handler instance. After that just call preload() on this handler:

    ./app/config/routes.js
    import { useComponentUtils } from '@ima/react-hooks';

    export default function Card() {
    const { $Router } = useComponentUtils();
    const homeRouteHandler = $Router.getRouteHandler('home');

    useEffect(() => {
    homeRouteHandler.preload();
    }, [])

    return (
    <a href={$Router.link('home')}>Home</a>
    );
    }

    The method returns a promise, which resolves to tuple of [controller, view] instances.

    Prefetching/Preloading modules

    As with the dynamic imports, you can also use webpack directives for prefetching and preloading. Simply use the inline commend as it is mentioned in the webpack documentation.

    ./app/config/routes.js
    // ...
    async() => import(/* webpackPrefetch: true */ 'app/page/home/HomeController'),
    async() => import(/* webpackPreload: true */ 'app/page/home/HomeView')
    // ...
    + \ No newline at end of file diff --git a/basic-features/routing/dynamic-routes/index.html b/basic-features/routing/dynamic-routes/index.html index d8c8325c2..6de023bab 100644 --- a/basic-features/routing/dynamic-routes/index.html +++ b/basic-features/routing/dynamic-routes/index.html @@ -4,13 +4,13 @@ Dynamic Routes | IMA.js - +
    -

    Dynamic Routes

    Dynamic routes allows you to take control of route matching, route parameters parsing and generation of router links.

    They are really powerful and can help you cover those edge cases that cannot be done using basic string route expressions.

    This can be achieved by defining custom route matcher in form of a regular expression and custom functions to parse router params from path and, the other way, from route params to path.

    note

    The power of dynamic routes comes at a cost. You have to be really sure to define your matchers and function overrides correctly, so you don't end up with false positive route matches. We advise to cover these matchers heavily with tests in order to prevent potential failures.

    Creating Dynamic Routes

    Dynamic routes can be created just like the regular (static routes). The only thing that's different is the pathExpression positional argument, which is now object with three properties: matcher, toPath and extractParameters.

    The following example parses /category/subcategory/post/124 url formats with optional categories, and extract them along with the post itemId:

    ./app/config/routes.js
    import { AbstractRoute } from '@ima/core';

    import PostController from 'app/page/post/PostController';
    import PostView from 'app/page/post/PostView';

    const POST_MATCHER = /([\w-]+)?\/?([\w-]+)?\/post\/(\d+)/i;

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router.add(
    'post',
    {
    matcher: POST_MATCHER,
    extractParameters: (trimmedPath, ({ query, path })) => {
    const [match, category, subcategory, itemId] =
    POST_MATCHER.exec((trimmedPath)));

    return {
    category,
    subcategory,
    itemId,
    };
    },
    toPath: params => {
    const { category, subcategory, itemId, ...restParams } = params;
    const query = new URLSearchParams(restParams).toString();

    return [category, subcategory, itemId].filter(i => !!i).join('/') +
    (query ? `?${query}` : '');
    }
    },
    PostController,
    PostView
    );
    }
    info

    Notice that in the toPath function, we're appending other unused params to the final path as query params. This is to mimic the same functionality as the StaticRoutes provide by default.

    To make this process easier you can use the AbstractRoute.paramsToQuery() helper method, which filters and transforms object key-value pairs to query params string.

    matcher

    RegExp

    Regular expression used in route matching. The router tries to match path, stripped from trailing slashes, against this regular expression.

    extractParameters

    (trimmedPath: string, { query: RouteParams; path: string }) => RouteParams

    Function used to extract route params from given path. It receives path trimmed from trailing slashes and query params as first argument.

    For more control, you can use additional data in form of query and path which contain query params extracted from trimmed path and full path without any modifications.

    note

    When using StaticRoutes, query parameters are automatically merged with extracted route params. If you want to mimic this behavior, don't forget to merge query params into your final route params object.

    toPath

    (params: RouteParams) => string

    Function used to create path from given params (including query params). It is used mainly in the router link creation.

    note

    It is a good practice to append any unused params as query params to the path (you can use the static AbstractRoute.paramsToQuery() static helper to do that).

    - +

    Dynamic Routes

    Dynamic routes allows you to take control of route matching, route parameters parsing and generation of router links.

    They are really powerful and can help you cover those edge cases that cannot be done using basic string route expressions.

    This can be achieved by defining custom route matcher in form of a regular expression and custom functions to parse router params from path and, the other way, from route params to path.

    note

    The power of dynamic routes comes at a cost. You have to be really sure to define your matchers and function overrides correctly, so you don't end up with false positive route matches. We advise to cover these matchers heavily with tests in order to prevent potential failures.

    Creating Dynamic Routes

    Dynamic routes can be created just like the regular (static routes). The only thing that's different is the pathExpression positional argument, which is now object with three properties: matcher, toPath and extractParameters.

    The following example parses /category/subcategory/post/124 url formats with optional categories, and extract them along with the post itemId:

    ./app/config/routes.js
    import { AbstractRoute } from '@ima/core';

    import PostController from 'app/page/post/PostController';
    import PostView from 'app/page/post/PostView';

    const POST_MATCHER = /([\w-]+)?\/?([\w-]+)?\/post\/(\d+)/i;

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router.add(
    'post',
    {
    matcher: POST_MATCHER,
    extractParameters: (trimmedPath, ({ query, path })) => {
    const [match, category, subcategory, itemId] =
    POST_MATCHER.exec((trimmedPath)));

    return {
    category,
    subcategory,
    itemId,
    };
    },
    toPath: params => {
    const { category, subcategory, itemId, ...restParams } = params;
    const query = new URLSearchParams(restParams).toString();

    return [category, subcategory, itemId].filter(i => !!i).join('/') +
    (query ? `?${query}` : '');
    }
    },
    PostController,
    PostView
    );
    }
    info

    Notice that in the toPath function, we're appending other unused params to the final path as query params. This is to mimic the same functionality as the StaticRoutes provide by default.

    To make this process easier you can use the AbstractRoute.paramsToQuery() helper method, which filters and transforms object key-value pairs to query params string.

    matcher

    RegExp

    Regular expression used in route matching. The router tries to match path, stripped from trailing slashes, against this regular expression.

    extractParameters

    (trimmedPath: string, { query: RouteParams; path: string }) => RouteParams

    Function used to extract route params from given path. It receives path trimmed from trailing slashes and query params as first argument.

    For more control, you can use additional data in form of query and path which contain query params extracted from trimmed path and full path without any modifications.

    note

    When using StaticRoutes, query parameters are automatically merged with extracted route params. If you want to mimic this behavior, don't forget to merge query params into your final route params object.

    toPath

    (params: RouteParams) => string

    Function used to create path from given params (including query params). It is used mainly in the router link creation.

    note

    It is a good practice to append any unused params as query params to the path (you can use the static AbstractRoute.paramsToQuery() static helper to do that).

    + \ No newline at end of file diff --git a/basic-features/routing/introduction/index.html b/basic-features/routing/introduction/index.html index 72c52fab8..fb2685545 100644 --- a/basic-features/routing/introduction/index.html +++ b/basic-features/routing/introduction/index.html @@ -4,13 +4,13 @@ Introduction | IMA.js - +
    -

    Introduction

    Routing is an essential part of every application that displays multiple pages. It allows to develop each part of an application separately and add new parts instantly. As it happens to be in MVC frameworks, each route targets specific controller which takes control over what happens next after a route is matched.

    Setting up Router

    All routes in IMA.js are registered inside the init function in app/config/routes.js. Same init function can be found in app/config/bind.js. See Object Container documentation for more information about the oc.get() function.

    Usually you should be oke with simple string defined StaticRoutes (the ones defined below), but the router also has support for more advanced and powerful DynamicRoutes. For more information about these see the next section.

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    import HomeController from 'app/page/home/HomeController';
    import HomeView from 'app/page/home/HomeView';

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add('home', '/', HomeController, HomeView)
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
    }

    The router add method has following signature:

    add(name, pathExpression, controller, view, options = undefined);

    name

     string

    This argument represents unique route name. You can use this name when linking between routes or getting the route instance using getRouteHandler() method.

    pathExpression

    string | object

    This can be either object for dynamic routes or string representing route path. The pathExpression supports **parameter substitutions

    controller

    string | function

    Route assigned Controller class (can be a string alias, referring to the controller registered in the Object Container). It goes through its full lifecycle and renders the View.

    view

    string | function

    Route assigned View class (also can be a string alias, referring to the view registered in the Object Container). Rendered by the route controller.

    options

    object = undefined

    These are optional, however it accepts object with following properties and their respective defaults:

    {
    onlyUpdate: false,
    autoScroll: true,
    allowSPA: true,
    documentView: null,
    managedRootView: null,
    viewAdapter: null,
    middlewares: []
    }

    onlyUpdate

    boolean | function = false

    When only the parameters of the current route change an update method of the active controller will be invoked instead of re-instantiating the controller and view. The update method receives prevParams object containing - as the name suggests - previous route parameters.

    If you provide function to the onlyUpdate option; it receives 2 arguments (instances of previous controller and view) and it should return boolean.

    autoScroll

    boolean = true

    Determines whether the page should be scrolled to the top when the navigation occurs.

    allowSPA

    boolean = true

    Can be used to make the route to be always served from the server and never using the SPA (when disabled) even if the server is overloaded.

    This is useful for routes that use different document views (specified by the documentView option), for example for rendering the content of iframes.

    documentView

    AbstractDocumentView = null

    Custom DocumentView, should extend the AbstractDocumentView from @ima/core.

    managedRootView

    function = null

    Custom ManagedRootView component, for more information see rendering process.

    viewAdapter

    function = null

    Custom ViewAdapter component, for more information see rendering process.

    middlewares

    function[] = []

    Array of route-specific middlewares. See the middlewares section for more information.

    Route params substitutions

    The parameter name can contain only letters a-zA-Z, numbers 0-9, underscores _ and hyphens - and is preceded by colon :.

    router.add(
    'order-detail',
    '/user/:userId/orders/:orderId',
    OrderController,
    OrderView
    );

    The userId and orderId parameters are then accessible in OrderController via this.params:

    import { AbstractController } from '@ima/core';

    class OrderController extends AbstractController {
    load() {
    const userPromise = this._userService.get(this.params.userId);
    const orderPromise = this._orderService.get(this.params.orderId);

    return {
    user: userPromise,
    order: orderPromise
    }
    }
    }

    Optional parameters

    Parameters can also be marked as optional by placing question mark ? after the colon :.

    router.add(
    'user-detail',
    '/profile/:?userId',
    UserController,
    UserView
    );
    caution

    Optional parameters can be placed only after the last slash. Doing otherwise can cause unexpected behavior.

    Linking between routes

    URLs to routes can be generated via the Router.link() public method. These can be then used in ordinary anchor tags and IMA.js makes sure, to handle the site routing in SPA mode, rather than doing redirect/reload of the whole page.

    import { AbstractComponent } from '@ima/react-page-renderer';

    class OrderView extends AbstractComponent {
    render() {
    const { user, order } = this.props;

    const orderLink = this.link('order-detail', {
    userId: user.id,
    orderId: order.id
    });

    return <a href={orderLink}>View order</a>
    }
    }

    This is done by listening to window popstate and click events and reacting accordingly (in the listen method of ClientRouter, which is called by IMA.js on client during app init). If the handled URL is not valid registered app route, it is handled normally (e.g you are redirected to the target URL).

    tip

    You can use this.link helper method in IMA.js abstract component or the useLink hook from the @ima/react-hooks plugin in your components and views to generate router links.

    note

    Under the hood, this.link() is only alias for this.utils.$Router.link, where this.utils is taken from this.context.$Utils.

    For more information about this.utils and $Utils objects, take a look at the React Context in the documentation.

    Linking in Controllers, Extensions, Helpers and other Object Container classes requires you to import Router using dependency injection. To do that you can either use Router class in the dependency array, or $Router string alias:

    import { AbstractController } from '@ima/core';

    export default class DetailController extends AbstractController {
    static get $dependencies() {
    return ['$Router'];
    }

    constructor(router) {
    this._router = router;
    }

    load() {
    // ...
    }
    }

    Then you get Router instance as the constructor's first argument, which gives you access to it's link public method (and many others), that you can use to generate your desired route URL:

    load() {
    const detailLink = this._router.link('order-detail', {
    userId: user.id,
    orderId: order.id
    });

    return { detailLink };
    }

    Error and NotFound route names

    There are two special route names that @ima/core exports: RouteNames.ERROR, RouteNames.NOT_FOUND. You can use these constants to provide custom views and controllers for error handling pages.

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    import { ErrorController, ErrorView } from 'app/page/error';
    import { NotFoundController, NotFoundView } from 'app/page/not-found';

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add('home', '/', HomeController, HomeView)
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
    }

    Redirects

    In addition to the link method mentioned above (which handles URL generation for given routes), you can use Router.redirect() method to redirect directly to the targeted URL.

    This URL can be either existing app route or external URL. As with links, in this case you also get SPA routing, in case of redirection to different IMA.js app route.

    import { AbstractController, Router } from '@ima/core';

    export default class DetailController extends AbstractController {
    static get $dependencies() {
    return [
    Router // We're using class descriptor in this case for DI
    ];
    }

    constructor(router) {
    this._router = router;
    }

    init() {
    this._router.redirect(
    this._router.link('order-detail', {
    userId: user.id,
    orderId: order.id
    });
    );
    }
    }
    info

    On client side, redirections are handled by simply changing the window.location.href, while on server you're using the express native res.redirect method.

    Method signature

    The redirect method has following signature, while the options object is available only on server side:

    redirect(
    url = '',
    options = {} // Available only on server side
    )

    url

    string

    Target redirect URL.

    options

    object = {}

    Additional options, used to customize redirect server response.

    {
    httpStatus: 302,
    headers: undefined,
    }

    httpStatus

    number = 302

    Custom redirect http status code.

    headers

    object = undefined

    Custom response headers.

    - +

    Introduction

    Routing is an essential part of every application that displays multiple pages. It allows to develop each part of an application separately and add new parts instantly. As it happens to be in MVC frameworks, each route targets specific controller which takes control over what happens next after a route is matched.

    Setting up Router

    All routes in IMA.js are registered inside the init function in app/config/routes.js. Same init function can be found in app/config/bind.js. See Object Container documentation for more information about the oc.get() function.

    Usually you should be oke with simple string defined StaticRoutes (the ones defined below), but the router also has support for more advanced and powerful DynamicRoutes. For more information about these see the next section.

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    import HomeController from 'app/page/home/HomeController';
    import HomeView from 'app/page/home/HomeView';

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add('home', '/', HomeController, HomeView)
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
    }

    The router add method has following signature:

    add(name, pathExpression, controller, view, options = undefined);

    name

     string

    This argument represents unique route name. You can use this name when linking between routes or getting the route instance using getRouteHandler() method.

    pathExpression

    string | object

    This can be either object for dynamic routes or string representing route path. The pathExpression supports **parameter substitutions

    controller

    string | function

    Route assigned Controller class (can be a string alias, referring to the controller registered in the Object Container). It goes through its full lifecycle and renders the View.

    view

    string | function

    Route assigned View class (also can be a string alias, referring to the view registered in the Object Container). Rendered by the route controller.

    options

    object = undefined

    These are optional, however it accepts object with following properties and their respective defaults:

    {
    onlyUpdate: false,
    autoScroll: true,
    allowSPA: true,
    documentView: null,
    managedRootView: null,
    viewAdapter: null,
    middlewares: []
    }

    onlyUpdate

    boolean | function = false

    When only the parameters of the current route change an update method of the active controller will be invoked instead of re-instantiating the controller and view. The update method receives prevParams object containing - as the name suggests - previous route parameters.

    If you provide function to the onlyUpdate option; it receives 2 arguments (instances of previous controller and view) and it should return boolean.

    autoScroll

    boolean = true

    Determines whether the page should be scrolled to the top when the navigation occurs.

    allowSPA

    boolean = true

    Can be used to make the route to be always served from the server and never using the SPA (when disabled) even if the server is overloaded.

    This is useful for routes that use different document views (specified by the documentView option), for example for rendering the content of iframes.

    documentView

    AbstractDocumentView = null

    Custom DocumentView, should extend the AbstractDocumentView from @ima/core.

    managedRootView

    function = null

    Custom ManagedRootView component, for more information see rendering process.

    viewAdapter

    function = null

    Custom ViewAdapter component, for more information see rendering process.

    middlewares

    function[] = []

    Array of route-specific middlewares. See the middlewares section for more information.

    Route params substitutions

    The parameter name can contain only letters a-zA-Z, numbers 0-9, underscores _ and hyphens - and is preceded by colon :.

    router.add(
    'order-detail',
    '/user/:userId/orders/:orderId',
    OrderController,
    OrderView
    );

    The userId and orderId parameters are then accessible in OrderController via this.params:

    import { AbstractController } from '@ima/core';

    class OrderController extends AbstractController {
    load() {
    const userPromise = this._userService.get(this.params.userId);
    const orderPromise = this._orderService.get(this.params.orderId);

    return {
    user: userPromise,
    order: orderPromise
    }
    }
    }

    Optional parameters

    Parameters can also be marked as optional by placing question mark ? after the colon :.

    router.add(
    'user-detail',
    '/profile/:?userId',
    UserController,
    UserView
    );
    caution

    Optional parameters can be placed only after the last slash. Doing otherwise can cause unexpected behavior.

    Linking between routes

    URLs to routes can be generated via the Router.link() public method. These can be then used in ordinary anchor tags and IMA.js makes sure, to handle the site routing in SPA mode, rather than doing redirect/reload of the whole page.

    import { AbstractComponent } from '@ima/react-page-renderer';

    class OrderView extends AbstractComponent {
    render() {
    const { user, order } = this.props;

    const orderLink = this.link('order-detail', {
    userId: user.id,
    orderId: order.id
    });

    return <a href={orderLink}>View order</a>
    }
    }

    This is done by listening to window popstate and click events and reacting accordingly (in the listen method of ClientRouter, which is called by IMA.js on client during app init). If the handled URL is not valid registered app route, it is handled normally (e.g you are redirected to the target URL).

    tip

    You can use this.link helper method in IMA.js abstract component or the useLink hook from the @ima/react-hooks plugin in your components and views to generate router links.

    note

    Under the hood, this.link() is only alias for this.utils.$Router.link, where this.utils is taken from this.context.$Utils.

    For more information about this.utils and $Utils objects, take a look at the React Context in the documentation.

    Linking in Controllers, Extensions, Helpers and other Object Container classes requires you to import Router using dependency injection. To do that you can either use Router class in the dependency array, or $Router string alias:

    import { AbstractController } from '@ima/core';

    export default class DetailController extends AbstractController {
    static get $dependencies() {
    return ['$Router'];
    }

    constructor(router) {
    this._router = router;
    }

    load() {
    // ...
    }
    }

    Then you get Router instance as the constructor's first argument, which gives you access to it's link public method (and many others), that you can use to generate your desired route URL:

    load() {
    const detailLink = this._router.link('order-detail', {
    userId: user.id,
    orderId: order.id
    });

    return { detailLink };
    }

    Error and NotFound route names

    There are two special route names that @ima/core exports: RouteNames.ERROR, RouteNames.NOT_FOUND. You can use these constants to provide custom views and controllers for error handling pages.

    ./app/config/routes.js
    import { RouteNames } from '@ima/core';

    import { ErrorController, ErrorView } from 'app/page/error';
    import { NotFoundController, NotFoundView } from 'app/page/not-found';

    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .add('home', '/', HomeController, HomeView)
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
    }

    Redirects

    In addition to the link method mentioned above (which handles URL generation for given routes), you can use Router.redirect() method to redirect directly to the targeted URL.

    This URL can be either existing app route or external URL. As with links, in this case you also get SPA routing, in case of redirection to different IMA.js app route.

    import { AbstractController, Router } from '@ima/core';

    export default class DetailController extends AbstractController {
    static get $dependencies() {
    return [
    Router // We're using class descriptor in this case for DI
    ];
    }

    constructor(router) {
    this._router = router;
    }

    init() {
    this._router.redirect(
    this._router.link('order-detail', {
    userId: user.id,
    orderId: order.id
    });
    );
    }
    }
    info

    On client side, redirections are handled by simply changing the window.location.href, while on server you're using the express native res.redirect method.

    Method signature

    The redirect method has following signature, while the options object is available only on server side:

    redirect(
    url = '',
    options = {} // Available only on server side
    )

    url

    string

    Target redirect URL.

    options

    object = {}

    Additional options, used to customize redirect server response.

    {
    httpStatus: 302,
    headers: undefined,
    }

    httpStatus

    number = 302

    Custom redirect http status code.

    headers

    object = undefined

    Custom response headers.

    + \ No newline at end of file diff --git a/basic-features/routing/middlewares/index.html b/basic-features/routing/middlewares/index.html index 13a67c1ca..67014224d 100644 --- a/basic-features/routing/middlewares/index.html +++ b/basic-features/routing/middlewares/index.html @@ -4,13 +4,13 @@ Middlewares | IMA.js - +
    -

    Middlewares

    Middlewares are simple functions that run before/after route handlers. They can be used to restrict access to certain set of routes or act based on parsed route params.

    There are two types of middleware global and local. As the names suggest the first one is defined globally on the router instance using use() method and the second type is bound to specific route and is defined in the route options.middlewares property.

    ./app/config/routes.js
    // The imports are stripped for compactness.
    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .use(async (params, locals) => {
    console.log('Global middleware', params, locals, locals.route, locals.action);
    locals.counter = 0;
    });
    .add('home', '/', HomeController, HomeView, {
    middlewares: [
    async (params, locals, next) => {
    next({ counter: counter++ });
    }
    ]
    })
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
    }
    tip

    Since you have access to the object container (oc), you can basically do anything you want in the middlewares.

    You can easily define authentication middlewares or access-restricting middlewares since throwing an error from the middleware is handled the same way as any other error in the application.

    Function arguments

    Each middleware can be async and the functions can use up to three arguments: params, locals and next. params specifically allows you to modify route params, locals is used to pass data between middlewares and next callback provides additional

    params

    object = {}

    Contains route params extracted by the currently matched route handler. Can be empty if there was no route match before execution of concrete middleware.

    locals

    RouteLocals = {}

    Mutable object you can use to pass data between middlewares. It is passed across all middlewares, so anything you define here, is available in following middleware functions.

    tip

    In addition to mutating the original object, you can also return object values from middlewares or pass them as an argument in the next() function. These are then merged into the locals upon it's execution.

    async (params, locals) => {
    locals.counter++;
    }

    // or

    async (params, locals) => {
    return { counter: counter++ };
    }

    // or

    async (params, locals, next) => {
    next({ counter: counter++ });
    }

    Additionally it always contains following keys:

    route

    AbstractRoute

    Instance of currently matched route.

    action

    RouteAction = {}

    An action object describing what triggered this routing (can be empty).

    next

    (result?: object) => void

    When called, this function (as the name suggest) allows you to continue with execution of other route handlers. Apart from other frameworks that use similar feature, when you define next argument in your middleware, you have to execute it in order to continue. Otherwise the router will not proceed any further even if the middleware function content finished it's execution.

    This is intentional as it allows you to have more control over the middleware execution and gives you ability to stop the routing process completely.

    tip

    This is can be usefull in situations when for example you want to do a redirect, which is synchronous but takes a while until the window is reloaded. Without stopping the middleware execution (by defining the next callback and not calling it), you could get a glimpse of Error Page that is rendered before the redirect takes places, because the router continued it's processing.

    async (params, locals, next) => {
    if (await oc.get('User').isLoggedIn()) {
    // Continue normally
    return next();
    }

    // Stop execution by not calling `next()` and do a redirect
    oc.get('$Router').redirect('/');
    }

    Execution order

    Middleware functions are resolved from top to bottom sequentially. In case of the code above, when routing to home route, following things would have happened:

    1. Global middlewares defined above currently matched route are executed (in this case we have only one global middleware, defined above all routes).
    2. Params extraction from currently matched route handler (home) is executed.
    3. Local route middlewares are executed (with newly extracted route params).

    In case of an error or not found page, the execution order is still the same, meaning the global and route middlewares are executed as with any other route.

    caution

    There's only one exception, since the locals object is cleared to an empty object before route handling, if an error occurs during route handling and execution is internally passed to error handling (displaying error page), the locals object may retain values that were there for the previous route matching. However the locals.route object will still be up to date and equal to currently routed route (error in this case).

    Execution timeout

    To prevent middlewares from freezing the application, for example when the middlewares takes too long to execute, we've implemented execution timeout, which prevents them from running indefinitely.

    You can customize the timeout value in app settings:

    ./app/config/settings.js
    export default (ns, oc, config) => {
    return {
    prod: {
    $Router: {
    middlewareTimeout: 30000, // ms
    },
    },
    };
    };
    - +

    Middlewares

    Middlewares are simple functions that run before/after route handlers. They can be used to restrict access to certain set of routes or act based on parsed route params.

    There are two types of middleware global and local. As the names suggest the first one is defined globally on the router instance using use() method and the second type is bound to specific route and is defined in the route options.middlewares property.

    ./app/config/routes.js
    // The imports are stripped for compactness.
    export let init = (ns, oc, config) => {
    const router = oc.get('$Router');

    router
    .use(async (params, locals) => {
    console.log('Global middleware', params, locals, locals.route, locals.action);
    locals.counter = 0;
    });
    .add('home', '/', HomeController, HomeView, {
    middlewares: [
    async (params, locals, next) => {
    next({ counter: counter++ });
    }
    ]
    })
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
    }
    tip

    Since you have access to the object container (oc), you can basically do anything you want in the middlewares.

    You can easily define authentication middlewares or access-restricting middlewares since throwing an error from the middleware is handled the same way as any other error in the application.

    Function arguments

    Each middleware can be async and the functions can use up to three arguments: params, locals and next. params specifically allows you to modify route params, locals is used to pass data between middlewares and next callback provides additional

    params

    object = {}

    Contains route params extracted by the currently matched route handler. Can be empty if there was no route match before execution of concrete middleware.

    locals

    RouteLocals = {}

    Mutable object you can use to pass data between middlewares. It is passed across all middlewares, so anything you define here, is available in following middleware functions.

    tip

    In addition to mutating the original object, you can also return object values from middlewares or pass them as an argument in the next() function. These are then merged into the locals upon it's execution.

    async (params, locals) => {
    locals.counter++;
    }

    // or

    async (params, locals) => {
    return { counter: counter++ };
    }

    // or

    async (params, locals, next) => {
    next({ counter: counter++ });
    }

    Additionally it always contains following keys:

    route

    AbstractRoute

    Instance of currently matched route.

    action

    RouteAction = {}

    An action object describing what triggered this routing (can be empty).

    next

    (result?: object) => void

    When called, this function (as the name suggest) allows you to continue with execution of other route handlers. Apart from other frameworks that use similar feature, when you define next argument in your middleware, you have to execute it in order to continue. Otherwise the router will not proceed any further even if the middleware function content finished it's execution.

    This is intentional as it allows you to have more control over the middleware execution and gives you ability to stop the routing process completely.

    tip

    This is can be usefull in situations when for example you want to do a redirect, which is synchronous but takes a while until the window is reloaded. Without stopping the middleware execution (by defining the next callback and not calling it), you could get a glimpse of Error Page that is rendered before the redirect takes places, because the router continued it's processing.

    async (params, locals, next) => {
    if (await oc.get('User').isLoggedIn()) {
    // Continue normally
    return next();
    }

    // Stop execution by not calling `next()` and do a redirect
    oc.get('$Router').redirect('/');
    }

    Execution order

    Middleware functions are resolved from top to bottom sequentially. In case of the code above, when routing to home route, following things would have happened:

    1. Global middlewares defined above currently matched route are executed (in this case we have only one global middleware, defined above all routes).
    2. Params extraction from currently matched route handler (home) is executed.
    3. Local route middlewares are executed (with newly extracted route params).

    In case of an error or not found page, the execution order is still the same, meaning the global and route middlewares are executed as with any other route.

    caution

    There's only one exception, since the locals object is cleared to an empty object before route handling, if an error occurs during route handling and execution is internally passed to error handling (displaying error page), the locals object may retain values that were there for the previous route matching. However the locals.route object will still be up to date and equal to currently routed route (error in this case).

    Execution timeout

    To prevent middlewares from freezing the application, for example when the middlewares takes too long to execute, we've implemented execution timeout, which prevents them from running indefinitely.

    You can customize the timeout value in app settings:

    ./app/config/settings.js
    export default (ns, oc, config) => {
    return {
    prod: {
    $Router: {
    middlewareTimeout: 30000, // ms
    },
    },
    };
    };
    + \ No newline at end of file diff --git a/basic-features/seo-and-meta-manager/index.html b/basic-features/seo-and-meta-manager/index.html index 5e2dd5c9c..68229c6bc 100644 --- a/basic-features/seo-and-meta-manager/index.html +++ b/basic-features/seo-and-meta-manager/index.html @@ -4,7 +4,7 @@ SEO & Meta Manager | IMA.js - + @@ -13,8 +13,8 @@ probably come along the setMetaParams method. This method is dedicated to set meta information for a specific page and you are provided with everything you need (current state, MetaManager, -router, dictionary and settings).

    Meta manager offers many methods to work with document meta data. From #{meta} content variable, to methods for managing title and other meta tags collections.

    Managing meta tags

    As mentioned above, all meta management is done in setMetaParams method in route controller. Using metaManager and provied setters for title, meta name, meta properties and link collections, you can manage contents of your meta tags easily with the help of additional arguments that provide everything you need (current state, MetaManager, router, dictionary and settings).

    ./app/page/order/OrderController.js
    setMetaParams(loadedResources, metaManager, router, dictionary, settings) {
    const { order } = loadedResources;

    metaManager.setTitle(`Order #${order.id} - ${settings.general.appTitle}`);
    metaManager.setMetaName(
    'description',
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
    );

    metaManager.setMetaProperty('og:image', order.thumbnailUrl);

    const orderDetailLink = router.link('order-detail', {
    orderId: order.id
    });

    metaManager.setLink('canonical', orderDetailLink);
    }
    info

    undefined and null values are filtered out when rendering meta tags. If you still want to render meta tags with empty values, use empty strings ''.

    setTitle()

    (title: string) => MetaManager

    Use to set document title.

    setMetaName()

    (name: string, content: MetaValue, attr?: MetaAttributes) => MetaManager

    Sets the information to be used in <meta name="..." content="..." />.

    setMetaProperty()

    (name: string, property: MetaValue, attr?: MetaAttributes) => MetaManager

    These methods are similar to the two above except that these are used for <meta property="..." content="..." />.

    (relation: string, href: MetaValue, attr?: MetaAttributes) => MetaManager

    Adds information to the MetaManager to be later used in <link rel="..." href="..." /> tag.

    tip

    All 3 methods defined above also supports additional optional attributes. This is an object of key-value pairs representing additional meta tag attributes that are used in certain situations.


    metaManager.setMetaProperty('og:image', order.thumbnailUrl, {
    size: 'large',
    authorUrl: 'https://mysite.com'
    });

    Meta value getters

    Each setter has corresponding getter returning and object with key-value pairs representing the meta tag values. Additionally you can use key and value iterator methods.

    • getTitle()
    • getMetaName(), getMetaNames(), getMetaNamesIterator()
    • getMetaProperty(), setMetaProperties(), setMetaPropertiesIterator()
    • getLink(), getLinks(), getLinksIterator()
    tip

    Since the getter methods return object with key-value attributes where their names correspond to the html tag attribute name, you can use following shortcuts to render (these include optional attributes):

    <meta
    property="og:image"
    {...this.props.metaManager.getMetaProperty('og:image')}
    />
    <meta
    link="canonical"
    {...this.props.metaManager.getLink('canonical')}
    />

    Rendering meta tags

    Meta tags are handled differently on server an client, see following sections for more information on this matter.

    Rendering on server using the #{meta} content variable

    While you can manually render meta tags in the document view using metaManger and any of the provided getter methods or iterators, we also render these tags automatically into #{meta} content variable.

    You can then use this content variable in DocumentView to easily render whole meta collection (including document title) matching meta information set for current controller in setMetaParams method.

    ./app/document/DocumentView.jsx
    <head>
    {'#{meta}'}
    {'#{styles}'}
    {'#{revivalSettings}'}
    {'#{runner}'}
    </head>
    note

    While you can also use this content variable in spa.ejs, it will always be empty, since client rendering is handled separately. See the next section for more information.

    Rendering on client using PageMetaHandler

    You may have noticed that the server-side rendered meta tags have data-ima-meta data attribute. This serves as an identification for meta tags that are handled by IMA.js (both on server and client). These also correspond to the values you have set using metaManager setters.

    <meta data-ima-meta name="twitter:title" content="IMA.js">

    While navigating between pages in SPA, the meta tags are updated automatically using PageMetaHandler. This manager always removes old meta tags identified by the data attribute, before rendering new ones. And since metaManager clears it's meta collection between routes, this means that each page renders only those tags that are set in metaManager in current page controller using setMetaParams method.

    Global meta tags

    Now that you know how IMA.js handles meta tag updates between routes, you may ask yourself a question "how to handle global meta tags like viewport, charset etc.?"

    The solution is pretty simple - just define them in DocumentView and spa.ejs templates, tags that don't have data-ima-meta attribute are not touched at all by the PageMetaHandler.

    ./app/document/DocumentView.jsx
    <head>
    <meta charSet='utf-8' />
    <meta httpEquiv='X-UA-Compatible' content='IE=edge' />
    <meta name='viewport' content='width=device-width, initial-scale=1' />
    </head>

    Alternative solution is to always set these values in every page controller you have. For this you can use helpers or create custom AbstractController.

    - +router, dictionary and settings).

    Meta manager offers many methods to work with document meta data. From #{meta} content variable, to methods for managing title and other meta tags collections.

    Managing meta tags

    As mentioned above, all meta management is done in setMetaParams method in route controller. Using metaManager and provied setters for title, meta name, meta properties and link collections, you can manage contents of your meta tags easily with the help of additional arguments that provide everything you need (current state, MetaManager, router, dictionary and settings).

    ./app/page/order/OrderController.js
    setMetaParams(loadedResources, metaManager, router, dictionary, settings) {
    const { order } = loadedResources;

    metaManager.setTitle(`Order #${order.id} - ${settings.general.appTitle}`);
    metaManager.setMetaName(
    'description',
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
    );

    metaManager.setMetaProperty('og:image', order.thumbnailUrl);

    const orderDetailLink = router.link('order-detail', {
    orderId: order.id
    });

    metaManager.setLink('canonical', orderDetailLink);
    }
    info

    undefined and null values are filtered out when rendering meta tags. If you still want to render meta tags with empty values, use empty strings ''.

    setTitle()

    (title: string) => MetaManager

    Use to set document title.

    setMetaName()

    (name: string, content: MetaValue, attr?: MetaAttributes) => MetaManager

    Sets the information to be used in <meta name="..." content="..." />.

    setMetaProperty()

    (name: string, property: MetaValue, attr?: MetaAttributes) => MetaManager

    These methods are similar to the two above except that these are used for <meta property="..." content="..." />.

    (relation: string, href: MetaValue, attr?: MetaAttributes) => MetaManager

    Adds information to the MetaManager to be later used in <link rel="..." href="..." /> tag.

    tip

    All 3 methods defined above also supports additional optional attributes. This is an object of key-value pairs representing additional meta tag attributes that are used in certain situations.


    metaManager.setMetaProperty('og:image', order.thumbnailUrl, {
    size: 'large',
    authorUrl: 'https://mysite.com'
    });

    Meta value getters

    Each setter has corresponding getter returning and object with key-value pairs representing the meta tag values. Additionally you can use key and value iterator methods.

    • getTitle()
    • getMetaName(), getMetaNames(), getMetaNamesIterator()
    • getMetaProperty(), setMetaProperties(), setMetaPropertiesIterator()
    • getLink(), getLinks(), getLinksIterator()
    tip

    Since the getter methods return object with key-value attributes where their names correspond to the html tag attribute name, you can use following shortcuts to render (these include optional attributes):

    <meta
    property="og:image"
    {...this.props.metaManager.getMetaProperty('og:image')}
    />
    <meta
    link="canonical"
    {...this.props.metaManager.getLink('canonical')}
    />

    Rendering meta tags

    Meta tags are handled differently on server an client, see following sections for more information on this matter.

    Rendering on server using the #{meta} content variable

    While you can manually render meta tags in the document view using metaManger and any of the provided getter methods or iterators, we also render these tags automatically into #{meta} content variable.

    You can then use this content variable in DocumentView to easily render whole meta collection (including document title) matching meta information set for current controller in setMetaParams method.

    ./app/document/DocumentView.jsx
    <head>
    {'#{meta}'}
    {'#{styles}'}
    {'#{revivalSettings}'}
    {'#{runner}'}
    </head>
    note

    While you can also use this content variable in spa.ejs, it will always be empty, since client rendering is handled separately. See the next section for more information.

    Rendering on client using PageMetaHandler

    You may have noticed that the server-side rendered meta tags have data-ima-meta data attribute. This serves as an identification for meta tags that are handled by IMA.js (both on server and client). These also correspond to the values you have set using metaManager setters.

    <meta data-ima-meta name="twitter:title" content="IMA.js">

    While navigating between pages in SPA, the meta tags are updated automatically using PageMetaHandler. This manager always removes old meta tags identified by the data attribute, before rendering new ones. And since metaManager clears it's meta collection between routes, this means that each page renders only those tags that are set in metaManager in current page controller using setMetaParams method.

    Global meta tags

    Now that you know how IMA.js handles meta tag updates between routes, you may ask yourself a question "how to handle global meta tags like viewport, charset etc.?"

    The solution is pretty simple - just define them in DocumentView and spa.ejs templates, tags that don't have data-ima-meta attribute are not touched at all by the PageMetaHandler.

    ./app/document/DocumentView.jsx
    <head>
    <meta charSet='utf-8' />
    <meta httpEquiv='X-UA-Compatible' content='IE=edge' />
    <meta name='viewport' content='width=device-width, initial-scale=1' />
    </head>

    Alternative solution is to always set these values in every page controller you have. For this you can use helpers or create custom AbstractController.

    + \ No newline at end of file diff --git a/basic-features/testing/index.html b/basic-features/testing/index.html index e3191b420..843aca059 100644 --- a/basic-features/testing/index.html +++ b/basic-features/testing/index.html @@ -4,13 +4,13 @@ Testing | IMA.js - +
    -

    Testing

    The @ima/testing-library contains utilities for testing IMA.js applications. It provides integration with Jest, React Testing Library and Testing Library Jest DOM. If you initialized your project via create-ima-app, the testing setup is already included in your project. If not, check @ima/testing-library README for more information about how to setup testing in your project.

    API

    IMA Testing Library is re-exporting everything from @testing-library/react. You should always import React Testing Library functions from @ima/testing-library as we might add some additional functionality / wrappers in the future. As such, it provides the same API as @testing-library/react with some additional features.

    renderWithContext

    async function renderWithContext(
    ui: ReactElement,
    options?: RenderOptions & { contextValue?: ContextValue; app?: ImaApp }
    ): Promise<ReturnType<typeof render> & { app: ImaApp | null; contextValue: ContextValue; }>

    renderWithContext is a wrapper around render from @testing-library/react. It sets wrapper option in render method to a real IMA.js context wrapper. It can take additional optional IMA specific options:

    • contextValue - the result of getContextValue
    • app - the result of initImaApp (if you provide contextValue, it does not make any sense to provide app as the app is only used to generate the contextValue)

    If any of the options is not provided, it will be generated automatically.

    Example usage:

    import { useLocalize } from '@ima/react-page-renderer';
    import { renderWithContext } from '@ima/testing-library';

    function Component({ children }) {
    const localize = useLocalize(); // Get localize function from IMA.js context

    return <div>{localize('my.translation.key')} {children}</div>;
    }

    test('renders component with localized string', async () => {
    const { getByText } = await renderWithContext(<Component>My Text</Component>);
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    If you had used render from @testing-library/react directly, the test would have failed because the useLocalize hook would be missing the context. The renderWithContext function provides the necessary context and making it easier to test components that depend on the IMA.js context.

    getContextValue

    async function getContextValue(app?: ImaApp): Promise<ContextValue>

    getContextValue is a helper function that returns the context value from the IMA.js app. It can take an optional app parameter, which is the result of initImaApp.

    Example usage:

    test('renders component with custom context value', async () => {
    const contextValue = await getContextValue(); // Generate default context value

    contextValue.$Utils.$Foo = jest.fn(() => 'bar'); // Mock some part of the context

    const { getByText } = await renderWithContext(<Component>My Text</Component>, {
    contextValue, // Provide the custom context value
    });
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    initImaApp

    async function initImaApp(): Promise<ImaApp>

    initImaApp is a helper function that initializes the IMA.js app.

    test('renders component with custom app configuration', async () => {
    const app = await initImaApp(); // Initialize the app

    app.oc.get('$Utils').$Foo = jest.fn(() => 'bar'); // Mock some part of the app

    const { getByText } = await renderWithContext(<Component>My Text</Component>, {
    app, // Provide the custom app
    });
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    renderHookWithContext

    async function renderHookWithContext<TResult, TProps>(
    hook: (props: TProps) => TResult,
    options?: { contextValue?: ContextValue; app?: ImaApp }
    ): Promise<ReturnType<typeof renderHook<TResult, TProps>> & { app: ImaApp | null; contextValue: ContextValue; }>

    renderHookWithContext is a wrapper around renderHook from @testing-library/react. It uses the same logic as renderWithContext to provide the IMA.js context. See the renderWithContext section for more information.

    Extending IMA boot config methods

    You can extend IMA boot config by using IMA pluginLoader.register method. Use the same approach as in IMA plugins.

    You can either register a plugin loader for all tests by setting it up in a setup file.

    // jestSetup.js
    import { pluginLoader } from '@ima/core';

    // If you don't care, if this plugin loader is registered first, or last
    pluginLoader.register('jestSetup.js', () => {
    return {
    initSettings: () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    }
    };
    });

    // If you need to register the plugin loader after all other plugin loaders
    beforeAll(() => {
    pluginLoader.register('jestSetup.js', () => {
    return {
    initSettings: () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    }
    };
    });
    });

    // jest.config.js
    module.exports = {
    // Add this line to your jest config
    setupFilesAfterEnv: ['./jestSetup.js']
    };

    Or you can register a plugin loader for a specific test file.

    // mySpec.js
    import { pluginLoader } from '@ima/core';

    beforeAll(() => {
    pluginLoader.register('mySpec', () => {
    return {
    initSettings: () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    }
    };
    });
    });

    test('renders component with custom app configuration', async () => {
    const { getByText } = await renderWithContext(<Component>My Text</Component>);
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    Or you can register a plugin loader for a test file, but make the boot config methods dynamic so you can change them for each test.

    // mySpec.js
    import { pluginLoader } from '@ima/core';

    // We create a placeholder for the plugin loader, so we can change it later
    let initSettings = () => {};

    beforeAll(() => {
    pluginLoader.register('mySpec', (...args) => {
    return {
    initSettings: (...args) => {
    return initSettings(...args); // Here we call our overridable function
    }
    };
    });
    });

    afterEach(() => {
    initSettings = () => {}; // Reset the plugin loader so it is not called for other tests
    });

    test('renders component with custom app configuration', async () => {
    initSettings = () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    };

    const { getByText } = await renderWithContext(<Component>My Text</Component>);
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    Note, that the plugin loader register method evaluates the second argument right away, but the specific boot config methods are evaluated during renderWithContext (or initImaApp if you are using it directly).

    - +

    Testing

    The @ima/testing-library contains utilities for testing IMA.js applications. It provides integration with Jest, React Testing Library and Testing Library Jest DOM. If you initialized your project via create-ima-app, the testing setup is already included in your project. If not, check @ima/testing-library README for more information about how to setup testing in your project.

    API

    IMA Testing Library is re-exporting everything from @testing-library/react. You should always import React Testing Library functions from @ima/testing-library as we might add some additional functionality / wrappers in the future. As such, it provides the same API as @testing-library/react with some additional features.

    renderWithContext

    async function renderWithContext(
    ui: ReactElement,
    options?: RenderOptions & { contextValue?: ContextValue; app?: ImaApp }
    ): Promise<ReturnType<typeof render> & { app: ImaApp | null; contextValue: ContextValue; }>

    renderWithContext is a wrapper around render from @testing-library/react. It sets wrapper option in render method to a real IMA.js context wrapper. It can take additional optional IMA specific options:

    • contextValue - the result of getContextValue
    • app - the result of initImaApp (if you provide contextValue, it does not make any sense to provide app as the app is only used to generate the contextValue)

    If any of the options is not provided, it will be generated automatically.

    Example usage:

    import { useLocalize } from '@ima/react-page-renderer';
    import { renderWithContext } from '@ima/testing-library';

    function Component({ children }) {
    const localize = useLocalize(); // Get localize function from IMA.js context

    return <div>{localize('my.translation.key')} {children}</div>;
    }

    test('renders component with localized string', async () => {
    const { getByText } = await renderWithContext(<Component>My Text</Component>);
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    If you had used render from @testing-library/react directly, the test would have failed because the useLocalize hook would be missing the context. The renderWithContext function provides the necessary context and making it easier to test components that depend on the IMA.js context.

    getContextValue

    async function getContextValue(app?: ImaApp): Promise<ContextValue>

    getContextValue is a helper function that returns the context value from the IMA.js app. It can take an optional app parameter, which is the result of initImaApp.

    Example usage:

    test('renders component with custom context value', async () => {
    const contextValue = await getContextValue(); // Generate default context value

    contextValue.$Utils.$Foo = jest.fn(() => 'bar'); // Mock some part of the context

    const { getByText } = await renderWithContext(<Component>My Text</Component>, {
    contextValue, // Provide the custom context value
    });
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    initImaApp

    async function initImaApp(): Promise<ImaApp>

    initImaApp is a helper function that initializes the IMA.js app.

    test('renders component with custom app configuration', async () => {
    const app = await initImaApp(); // Initialize the app

    app.oc.get('$Utils').$Foo = jest.fn(() => 'bar'); // Mock some part of the app

    const { getByText } = await renderWithContext(<Component>My Text</Component>, {
    app, // Provide the custom app
    });
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    renderHookWithContext

    async function renderHookWithContext<TResult, TProps>(
    hook: (props: TProps) => TResult,
    options?: { contextValue?: ContextValue; app?: ImaApp }
    ): Promise<ReturnType<typeof renderHook<TResult, TProps>> & { app: ImaApp | null; contextValue: ContextValue; }>

    renderHookWithContext is a wrapper around renderHook from @testing-library/react. It uses the same logic as renderWithContext to provide the IMA.js context. See the renderWithContext section for more information.

    Extending IMA boot config methods

    You can extend IMA boot config by using IMA pluginLoader.register method. Use the same approach as in IMA plugins.

    You can either register a plugin loader for all tests by setting it up in a setup file.

    // jestSetup.js
    import { pluginLoader } from '@ima/core';

    // If you don't care, if this plugin loader is registered first, or last
    pluginLoader.register('jestSetup.js', () => {
    return {
    initSettings: () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    }
    };
    });

    // If you need to register the plugin loader after all other plugin loaders
    beforeAll(() => {
    pluginLoader.register('jestSetup.js', () => {
    return {
    initSettings: () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    }
    };
    });
    });

    // jest.config.js
    module.exports = {
    // Add this line to your jest config
    setupFilesAfterEnv: ['./jestSetup.js']
    };

    Or you can register a plugin loader for a specific test file.

    // mySpec.js
    import { pluginLoader } from '@ima/core';

    beforeAll(() => {
    pluginLoader.register('mySpec', () => {
    return {
    initSettings: () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    }
    };
    });
    });

    test('renders component with custom app configuration', async () => {
    const { getByText } = await renderWithContext(<Component>My Text</Component>);
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    Or you can register a plugin loader for a test file, but make the boot config methods dynamic so you can change them for each test.

    // mySpec.js
    import { pluginLoader } from '@ima/core';

    // We create a placeholder for the plugin loader, so we can change it later
    let initSettings = () => {};

    beforeAll(() => {
    pluginLoader.register('mySpec', (...args) => {
    return {
    initSettings: (...args) => {
    return initSettings(...args); // Here we call our overridable function
    }
    };
    });
    });

    afterEach(() => {
    initSettings = () => {}; // Reset the plugin loader so it is not called for other tests
    });

    test('renders component with custom app configuration', async () => {
    initSettings = () => {
    return {
    prod: {
    customSetting: 'customValue'
    }
    }
    };

    const { getByText } = await renderWithContext(<Component>My Text</Component>);
    const textElement = getByText(/My Text/i);

    expect(textElement).toBeInTheDocument();
    });

    Note, that the plugin loader register method evaluates the second argument right away, but the specific boot config methods are evaluated during renderWithContext (or initImaApp if you are using it directly).

    + \ No newline at end of file diff --git a/basic-features/typescript/index.html b/basic-features/typescript/index.html index 32d54eccb..f23a0ce79 100644 --- a/basic-features/typescript/index.html +++ b/basic-features/typescript/index.html @@ -4,13 +4,13 @@ TypeScript | IMA.js - +
    -

    TypeScript

    Since IMA.js v18 we provide support for Typescript in your application code with proper type declarations from the core packages.

    To enable TypeScript in your project, first you need to add typescript to your app dependencies:

    npm i -D typescript

    tsconfig.json

    Now create tsconfig.json file (that may look something like this):

    ./tsconfig.json
    {
    "compilerOptions": {
    "allowJs": true,
    "target": "ES2022",
    "lib": [
    "ES2022",
    "DOM",
    "DOM.Iterable"
    ],
    "module": "ES2022",
    "moduleResolution": "Node16",
    "strict": true,
    "resolveJsonModule": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "outDir": "./build/ts-cache",
    "paths": {
    "app/*": [
    "app/*"
    ],
    }
    },
    "include": ["./app/**/*", "./build/tmp/types/**/*"],
    "exclude": ["./**/__tests__"]
    }

    When CLI detects existence of the tsconfig.json file, it automatically starts type checking and compiling files with *.ts and *.tsx extensions.

    Keep in mind that the code is still compiled using swc, the same way JS code is. This means that certain settings in tsconfig.json only applies to type checking (like target, moduleResolution, etc.), but compilation uses it's own settings to match the JS code.

    tip

    You will also probably need to install additional @types/* type definition libs to ensure proper support, like react types:

    npm i -D @types/react @types/react-dom

    ima-env.d.ts

    Additionally we recommend creating a new ima-env.d.ts file in root of your ./app folder with following contents:

    ./app/ima-env.d.ts
    /// <reference types="@ima/cli/global" />

    This adds proper types support for webpack specific imports like images and other files.

    create-ima-app support

    You can also easily create a typescript base IMA.js application using --typescript cli argument when running create-ima-app command:

    npx create-ima-app ~/Desktop/ima-ts --typescript

    Controller generic types

    The AbstractController class follows similar principles used in React AbstractComponent type. There are 3 generic types you can define on the class definition itself.

    AbstractController.ts
    export class AbstractController<
    S extends PageState = {},
    R extends RouteParams = {},
    SS extends S = S
    > extends Controller<S, R, SS>;
    • S - Use to define shape of your controller managed state.
    • R - Use to define controller's route route params that are extracted to this.params.
    • SS - Defaults to S, however when you are using any extensions in your controller, that have their own state, you can merge those state types in this generic value, to have proper type support for this.getState() method (this will now include all state keys, including ones used in extensions).
    HomeController.ts
    import { TestExtension, GalleryExtensionState } from './GalleryExtension';

    export type HomeControllerState = {
    cards: Promise<CardData>;
    message: string;
    name: string;
    };

    export class HomeController extends AbstractController<
    HomeControllerState,
    { detailId?: string },
    HomeControllerState & GalleryExtensionState
    >{
    static $extensions?: Dependencies<Extension<any, any>> = [GalleryExtension];

    load(): HomeControllerState {
    const cardsPromise = this.#httpAgent
    .get<CardData>('http://localhost:3001/static/static/public/cards.json')
    .then(response => response.body);

    // `state` contains all merged types from `SS` generic value.
    const state = this.getState();

    return {
    message: 'test',
    cards: cardsPromise,
    name: 'nam',
    };
    }
    }

    Extending existing interfaces

    Since you can extend certain features like ComponentUtils or settings from within your application or through plugins, and in order to provide type checking for these, we are using specific interfaces that you can extend using Declaration Merging feature.

    This ensures (when used correctly), that you always have correct static types when using these interfaces, even when they are extended in multiple places.

    Extending Utils

    When using component utils, in addition to registering your classes using ComponentUtils helper, make sure to also extend Utils interface. This adds autocomplete and typechecking to this.utils() and useComponentUtils in your components.

    ./app/config/bind.ts
    declare module '@ima/core' {
    interface Utils {
    $CssClasses: typeof defaultCssClasses;
    }
    }

    export const initBindApp: InitBindFunction = (ns, oc) => {
    oc.get(ComponentUtils).register({
    $CssClasses: '$CssClasses',
    });
    };

    Extending ObjectContainer

    Same goes for defining string aliases in Object container. This adds proper type checking to dependencies definition and oc.get autocomplete.

    ./app/config/bind.ts
    declare module '@ima/core' {
    interface OCAliasMap {
    $CssClasses: () => typeof cssClassNameProcessor;
    $PageRendererFactory: PageRendererFactory;
    API_KEY: string;
    }
    }

    export const initBindApp: InitBindFunction = (ns, oc) => {
    oc.bind('$CssClasses', function () { return cssClassNameProcessor; });
    oc.bind('$PageRendererFactory', PageRendererFactory);
    oc.constant('API_KEY', '14fasdf');
    };

    Extending Settings

    This makes sure you don't have any missing or additional fields in your app settings. Other environments than prod have all fields made optional, since they are deeply merged with the prod settings.

    tip

    Use ?: for settings with default values. This applies mostly to plugins.

    ./app/config/settings.ts
    declare module '@ima/core' {
    interface Settings {
    links: Record<'documentation' | 'tutorial' | 'plugins' | 'api', string>;
    }
    }

    export const initSettings: InitSettingsFunction = (ns, oc, config) => {
    return {
    prod: {
    links: {
    documentation: 'https://imajs.io/docs',
    api: 'https://imajs.io/api',
    },
    }
    }
    }

    Dictionary localization keys

    When compiling app language files, we also generate dictionary keys during runtime. These are then stored in './build/tmp/types/dictionary.ts' file. Don't forget to include this file in tsconfig.json source files array, to have correct static type checking:

    ./tsconfig.json
    {
    "include": ["./app/**/*", "./build/tmp/types/**/*"],
    }
    note

    When used in IMA.js plugins, you can manually extend the DictionaryMap interface:

    declare module '@ima/core' {
    interface DictionaryMap {
    'home.intro': string;
    }
    }

    export {};
    - +

    TypeScript

    Since IMA.js v18 we provide support for Typescript in your application code with proper type declarations from the core packages.

    To enable TypeScript in your project, first you need to add typescript to your app dependencies:

    npm i -D typescript

    tsconfig.json

    Now create tsconfig.json file (that may look something like this):

    ./tsconfig.json
    {
    "compilerOptions": {
    "allowJs": true,
    "target": "ES2022",
    "lib": [
    "ES2022",
    "DOM",
    "DOM.Iterable"
    ],
    "module": "ES2022",
    "moduleResolution": "Node16",
    "strict": true,
    "resolveJsonModule": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "outDir": "./build/ts-cache",
    "paths": {
    "app/*": [
    "app/*"
    ],
    }
    },
    "include": ["./app/**/*", "./build/tmp/types/**/*"],
    "exclude": ["./**/__tests__"]
    }

    When CLI detects existence of the tsconfig.json file, it automatically starts type checking and compiling files with *.ts and *.tsx extensions.

    Keep in mind that the code is still compiled using swc, the same way JS code is. This means that certain settings in tsconfig.json only applies to type checking (like target, moduleResolution, etc.), but compilation uses it's own settings to match the JS code.

    tip

    You will also probably need to install additional @types/* type definition libs to ensure proper support, like react types:

    npm i -D @types/react @types/react-dom

    ima-env.d.ts

    Additionally we recommend creating a new ima-env.d.ts file in root of your ./app folder with following contents:

    ./app/ima-env.d.ts
    /// <reference types="@ima/cli/global" />

    This adds proper types support for webpack specific imports like images and other files.

    create-ima-app support

    You can also easily create a typescript base IMA.js application using --typescript cli argument when running create-ima-app command:

    npx create-ima-app ~/Desktop/ima-ts --typescript

    Controller generic types

    The AbstractController class follows similar principles used in React AbstractComponent type. There are 3 generic types you can define on the class definition itself.

    AbstractController.ts
    export class AbstractController<
    S extends PageState = {},
    R extends RouteParams = {},
    SS extends S = S
    > extends Controller<S, R, SS>;
    • S - Use to define shape of your controller managed state.
    • R - Use to define controller's route route params that are extracted to this.params.
    • SS - Defaults to S, however when you are using any extensions in your controller, that have their own state, you can merge those state types in this generic value, to have proper type support for this.getState() method (this will now include all state keys, including ones used in extensions).
    HomeController.ts
    import { TestExtension, GalleryExtensionState } from './GalleryExtension';

    export type HomeControllerState = {
    cards: Promise<CardData>;
    message: string;
    name: string;
    };

    export class HomeController extends AbstractController<
    HomeControllerState,
    { detailId?: string },
    HomeControllerState & GalleryExtensionState
    >{
    static $extensions?: Dependencies<Extension<any, any>> = [GalleryExtension];

    load(): HomeControllerState {
    const cardsPromise = this.#httpAgent
    .get<CardData>('http://localhost:3001/static/static/public/cards.json')
    .then(response => response.body);

    // `state` contains all merged types from `SS` generic value.
    const state = this.getState();

    return {
    message: 'test',
    cards: cardsPromise,
    name: 'nam',
    };
    }
    }

    Extending existing interfaces

    Since you can extend certain features like ComponentUtils or settings from within your application or through plugins, and in order to provide type checking for these, we are using specific interfaces that you can extend using Declaration Merging feature.

    This ensures (when used correctly), that you always have correct static types when using these interfaces, even when they are extended in multiple places.

    Extending Utils

    When using component utils, in addition to registering your classes using ComponentUtils helper, make sure to also extend Utils interface. This adds autocomplete and typechecking to this.utils() and useComponentUtils in your components.

    ./app/config/bind.ts
    declare module '@ima/core' {
    interface Utils {
    $CssClasses: typeof defaultCssClasses;
    }
    }

    export const initBindApp: InitBindFunction = (ns, oc) => {
    oc.get(ComponentUtils).register({
    $CssClasses: '$CssClasses',
    });
    };

    Extending ObjectContainer

    Same goes for defining string aliases in Object container. This adds proper type checking to dependencies definition and oc.get autocomplete.

    ./app/config/bind.ts
    declare module '@ima/core' {
    interface OCAliasMap {
    $CssClasses: () => typeof cssClassNameProcessor;
    $PageRendererFactory: PageRendererFactory;
    API_KEY: string;
    }
    }

    export const initBindApp: InitBindFunction = (ns, oc) => {
    oc.bind('$CssClasses', function () { return cssClassNameProcessor; });
    oc.bind('$PageRendererFactory', PageRendererFactory);
    oc.constant('API_KEY', '14fasdf');
    };

    Extending Settings

    This makes sure you don't have any missing or additional fields in your app settings. Other environments than prod have all fields made optional, since they are deeply merged with the prod settings.

    tip

    Use ?: for settings with default values. This applies mostly to plugins.

    ./app/config/settings.ts
    declare module '@ima/core' {
    interface Settings {
    links: Record<'documentation' | 'tutorial' | 'plugins' | 'api', string>;
    }
    }

    export const initSettings: InitSettingsFunction = (ns, oc, config) => {
    return {
    prod: {
    links: {
    documentation: 'https://imajs.io/docs',
    api: 'https://imajs.io/api',
    },
    }
    }
    }

    Dictionary localization keys

    When compiling app language files, we also generate dictionary keys during runtime. These are then stored in './build/tmp/types/dictionary.ts' file. Don't forget to include this file in tsconfig.json source files array, to have correct static type checking:

    ./tsconfig.json
    {
    "include": ["./app/**/*", "./build/tmp/types/**/*"],
    }
    note

    When used in IMA.js plugins, you can manually extend the DictionaryMap interface:

    declare module '@ima/core' {
    interface DictionaryMap {
    'home.intro': string;
    }
    }

    export {};
    + \ No newline at end of file diff --git a/basic-features/views-and-components/index.html b/basic-features/views-and-components/index.html index 718dcde72..7d62cc6a2 100644 --- a/basic-features/views-and-components/index.html +++ b/basic-features/views-and-components/index.html @@ -4,7 +4,7 @@ Views & Components | IMA.js - + @@ -30,8 +30,8 @@ affecting how the rendered View looks and what it displays. A problem arises when a View wants to tell Controller to load or change something. The solution to this are event handling utils EventBus and -Dispatcher.

    Utilities shared across Views and Components

    At some point you'll come to a situation when it'd be nice to have a function or set of functions shared between multiple components. Great example would be custom link generation, page elements manipulation (modal, lightbox) or adverts and analytics.

    These cases are covered by ComponentUtils that allow you to register classes (utilities) that are then shared across every View and Component. Utilities are instantiated through OC therefore you can get access to other utilities or IMA.js components.

    Example Utility class would look like this. Simple class with dependency injection.

    // app/helper/LightboxHelper.js
    import { Router } from '@ima/core';

    export default class LightboxHelper {
    static get $dependencies() {
    return [Router];
    }

    showLightbox(content) {
    ...
    }
    }

    Then to register the utility class:

    // app/config/bind.js
    import { ComponentUtils } from '@ima/core';
    import LightboxHelper from 'app/helper/LightboxHelper';
    import AnalyticsUtils from 'app/helper/AnalyticsUtils';

    export default (ns, oc, config) => {
    const ComponentUtils = oc.get(ComponentUtils); // or oc.get('$ComponentUtils');

    ComponentUtils.register('Lightbox', LightboxHelper);
    // OR to register multiple utilities at once
    ComponentUtils.register({
    Lightbox: LightboxHelper,
    AnalyticsUtils
    });
    };

    Finally, what'd be the point to register these classes if we were not to use them... All of the utilities are present in utils property on AbstractComponent.

    // app/component/gallery/Gallery.jsx
    import { AbstractComponent } from '@ima/react-page-renderer';

    export default class Gallery extends AbstractComponent {

    onPhotoClick(photoId) {
    const { Lightbox } = this.utils;

    Lightbox.showLightbox(...);
    }
    }

    For some heavy-used utilities we've created a shortcut methods in AbstractComponent.

    One special case would be cssClasses shortcut which is by default alias for classnames package. You can overwrite this behavior by registering you own helper in ComponentUtils under $CssClasses alias.

    • cssClasses(classRules, includeComponentClassName = this.utils.$CssClasses()
    - +Dispatcher.

    Utilities shared across Views and Components

    At some point you'll come to a situation when it'd be nice to have a function or set of functions shared between multiple components. Great example would be custom link generation, page elements manipulation (modal, lightbox) or adverts and analytics.

    These cases are covered by ComponentUtils that allow you to register classes (utilities) that are then shared across every View and Component. Utilities are instantiated through OC therefore you can get access to other utilities or IMA.js components.

    Example Utility class would look like this. Simple class with dependency injection.

    // app/helper/LightboxHelper.js
    import { Router } from '@ima/core';

    export default class LightboxHelper {
    static get $dependencies() {
    return [Router];
    }

    showLightbox(content) {
    ...
    }
    }

    Then to register the utility class:

    // app/config/bind.js
    import { ComponentUtils } from '@ima/core';
    import LightboxHelper from 'app/helper/LightboxHelper';
    import AnalyticsUtils from 'app/helper/AnalyticsUtils';

    export default (ns, oc, config) => {
    const ComponentUtils = oc.get(ComponentUtils); // or oc.get('$ComponentUtils');

    ComponentUtils.register('Lightbox', LightboxHelper);
    // OR to register multiple utilities at once
    ComponentUtils.register({
    Lightbox: LightboxHelper,
    AnalyticsUtils
    });
    };

    Finally, what'd be the point to register these classes if we were not to use them... All of the utilities are present in utils property on AbstractComponent.

    // app/component/gallery/Gallery.jsx
    import { AbstractComponent } from '@ima/react-page-renderer';

    export default class Gallery extends AbstractComponent {

    onPhotoClick(photoId) {
    const { Lightbox } = this.utils;

    Lightbox.showLightbox(...);
    }
    }

    For some heavy-used utilities we've created a shortcut methods in AbstractComponent.

    One special case would be cssClasses shortcut which is by default alias for classnames package. You can overwrite this behavior by registering you own helper in ComponentUtils under $CssClasses alias.

    • cssClasses(classRules, includeComponentClassName = this.utils.$CssClasses()
    + \ No newline at end of file diff --git a/cli/additional-features/index.html b/cli/additional-features/index.html index 08f59e759..573500526 100644 --- a/cli/additional-features/index.html +++ b/cli/additional-features/index.html @@ -4,13 +4,13 @@ Additional features | IMA.js - +
    -

    Additional features

    This section describes multiple additional features that are either directly provided by the CLI or indirectly with the help of additional development packages @ima/hmr-client, @ima/error-overlay, @ima/dev-utils.

    Polyfills

    Sometimes you may need to include additional custom polyfills to fully support your application in multiple environments. There are multiple ways to achieve this.

    Static files in public folder

    The easies way, is to put your polyfill files directly into the app/public folder and load them either by extending the $Source configuration in the app environment or customizing the application's DocumentView.jsx and spa.html templates with custom script tags:

    ./app/document/DocumentView.jsx
    {/* ... */}
    <body>
    <script src='/static/public/custom-polyfill.js' />
    <div
    id={this.constructor.masterElementId}
    dangerouslySetInnerHTML={{ __html: this.props.page }}
    />
    <script
    id='revivalSettings'
    dangerouslySetInnerHTML={{ __html: this.props.revivalSettings }}
    />
    </body>
    {/* ... */}

    Importing polyfills at top of the main.js file

    Additionally you can import (or put) polyfills at the top of the ./app/main.js entry point.

    ./app/main.js
    import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';

    import './less/app.less';

    import * as ima from '@ima/core';
    import initBindApp from 'app/config/bind';
    //...

    Optional polyfill.js and polyfill.es.js entry points

    Lastly there are special polyfill.js and polyfill.es.js files that you can create in the root of the app directory. These, when bundled through webpack are available as separate JS files and are not part of the final app.bundle.js file.

    As with the previous option, you can either import the polyfills from the node_modules packages, or use their source code directly, by copying it into this file.

    info

    Both files are optional, this means that you can use, either one of those or don't use them at all. Similarly to the first option, don't forget to add the newly generated polyfill.js files somewhere in the source, so they are loaded on app startup.

    IMA.js Runtime

    In the Compiler features section, we mentioned that the CLI is compiling 3 separate bundles, mainly two distinct client bundles, where each targets certain ECMAScript version. This approach has 2 main advantages:

    1. We're still able to support pretty much every currently supported browser version (in case of the es2018 version).
    2. We're also serving the latest native version to the modern browsers that support's it (es2022 version). This bundle is also much smaller since it contains very low amount of core-js polyfills and should have better performance, because native implementations of existing APIs are usually faster than provided polyfills.

    You can customize source files for both versions in the $Source option of the app environment configuration file. This sources definition is then used by the IMA.js Runner, which then chooses (on the client side before app init) the most suitable version for the current browser environment and injects associated scripts into the DOM.

    This is done by executing few small scripts, where each script target's certain ECMAScript feature. Based on these results, the runner injects the best version of client bundle suitable for that concrete browser environment. It also makes sure to wait until all scripts have loaded before executing the webpack runtime.

    This makes sure that all external scripts that the app depends on (languages) are loaded correctly before it's execution and allows them to be loaded async to improve page load times.

    runner.ejs

    The IMA.js runner is simple snippet of JS code, that is injected into the page automatically within the app revivalSettings. It can be completely customizable by defining the runner overrides on the client window before it is injected into the DOM. It will then merge any existing overrides to the original runner before it's execution.

    info

    The runner script is intentionally written in es5 compatible syntax to make sure, that it can be executed on any environment and report using onUnsupported callback, when current browser can't even interpret the es2018 version of the bundle. In this case the application then runs in MPA mode

    Extending default script

    As mentioned before the runner script is fully extensible. For example to implement custom onUnsupported() and onError() callbacks, you'd do something like this:

    ./app/document/DocumentView.jsx
    <body>
    {/* ... */}
    <script>
    window.$IMA = window.$IMA || {};
    window.$IMA.$Runner = {
    /**
    * Optional onError handler. It is triggered in case the runtime
    * code fails to run the application.
    */
    onError: function (error) {
    throw new Error(error);
    },

    /**
    * Optional onUnsupported handler. It is triggered in case tests
    * for es and legacy version fails, which means that the APP runtime
    * code is never executed.
    */
    onUnsupported: function() {
    // Hit analytics with unsupported browser info
    },
    }
    </script>
    <script
    id='revivalSettings'
    dangerouslySetInnerHTML={{ __html: this.props.revivalSettings }}
    />
    </body>
    {/* ... */}

    Don't forget to apply the same changes to the spa.html, otherwise only SSR rendered pages will contain this override.

    note

    Since you will usually never want to change anything on the IMA.js Runner (apart from the previously mentioned callbacks), we won't go much deeper into it's the source code. You can always check it here and use it as a reference for custom overrides.

    However simply keeping your @ima/* dependencies up to will make sure, that you always receive the latest version of the runner script, which may evolve overtime.

    GenerateRuntimePlugin

    Since the webpack runtime is unique for every built, usually quite small, have to be loaded synchronously (which can impact the page load performance) and you would need to manually handle loading it's source code to the DOM, we have created GenerateRuntimePlugin to solve these issues.

    This plugin takes care of automatically generating the runtime consisting of the IMA.js runner code and webpack runtime (for both client bundles), which is then injected directly into the SPA template or SSR rendered html page. This means that you really don't have to worry about the existence of IMA.js runtime (while it's good to know that it does exist), since the framework handles all the hard work for you.

    It also comes with some performance benefits, since inlining these small scripts directly into the HTML removes the need to load additional 2 scripts synchronously, after browser parses the initial DOM.

    Dev server

    When you run your app using npx ima dev command, apart from building your application in development mode with HMR and all other dev features enabled, the CLI also starts companion express server - the dev server.

    By default it runs on http://localhost:3101 (this can be customized through ima.config.js or CLI options) and defines middlewares that are used mainly by the @ima/error-overlay (to properly display parsed error code snippets). Additionally it uses webpack-hot-middleware and webpack-dev-middleware to enable support for HMR.

    Using separate small server to host these middleware doesn't force us to define them directly on the app server, which could essentially produce some errors in certain situations.

    note

    Usually you don't have to think about the dev server during development and can simply pretend that it doesn't exist, since it is handled entirely by the CLI scripts. You can always have a look at the source code to learn more.

    - +

    Additional features

    This section describes multiple additional features that are either directly provided by the CLI or indirectly with the help of additional development packages @ima/hmr-client, @ima/error-overlay, @ima/dev-utils.

    Polyfills

    Sometimes you may need to include additional custom polyfills to fully support your application in multiple environments. There are multiple ways to achieve this.

    Static files in public folder

    The easies way, is to put your polyfill files directly into the app/public folder and load them either by extending the $Source configuration in the app environment or customizing the application's DocumentView.jsx and spa.html templates with custom script tags:

    ./app/document/DocumentView.jsx
    {/* ... */}
    <body>
    <script src='/static/public/custom-polyfill.js' />
    <div
    id={this.constructor.masterElementId}
    dangerouslySetInnerHTML={{ __html: this.props.page }}
    />
    <script
    id='revivalSettings'
    dangerouslySetInnerHTML={{ __html: this.props.revivalSettings }}
    />
    </body>
    {/* ... */}

    Importing polyfills at top of the main.js file

    Additionally you can import (or put) polyfills at the top of the ./app/main.js entry point.

    ./app/main.js
    import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';

    import './less/app.less';

    import * as ima from '@ima/core';
    import initBindApp from 'app/config/bind';
    //...

    Optional polyfill.js and polyfill.es.js entry points

    Lastly there are special polyfill.js and polyfill.es.js files that you can create in the root of the app directory. These, when bundled through webpack are available as separate JS files and are not part of the final app.bundle.js file.

    As with the previous option, you can either import the polyfills from the node_modules packages, or use their source code directly, by copying it into this file.

    info

    Both files are optional, this means that you can use, either one of those or don't use them at all. Similarly to the first option, don't forget to add the newly generated polyfill.js files somewhere in the source, so they are loaded on app startup.

    IMA.js Runtime

    In the Compiler features section, we mentioned that the CLI is compiling 3 separate bundles, mainly two distinct client bundles, where each targets certain ECMAScript version. This approach has 2 main advantages:

    1. We're still able to support pretty much every currently supported browser version (in case of the es2018 version).
    2. We're also serving the latest native version to the modern browsers that support's it (es2022 version). This bundle is also much smaller since it contains very low amount of core-js polyfills and should have better performance, because native implementations of existing APIs are usually faster than provided polyfills.

    You can customize source files for both versions in the $Source option of the app environment configuration file. This sources definition is then used by the IMA.js Runner, which then chooses (on the client side before app init) the most suitable version for the current browser environment and injects associated scripts into the DOM.

    This is done by executing few small scripts, where each script target's certain ECMAScript feature. Based on these results, the runner injects the best version of client bundle suitable for that concrete browser environment. It also makes sure to wait until all scripts have loaded before executing the webpack runtime.

    This makes sure that all external scripts that the app depends on (languages) are loaded correctly before it's execution and allows them to be loaded async to improve page load times.

    runner.ejs

    The IMA.js runner is simple snippet of JS code, that is injected into the page automatically within the app revivalSettings. It can be completely customizable by defining the runner overrides on the client window before it is injected into the DOM. It will then merge any existing overrides to the original runner before it's execution.

    info

    The runner script is intentionally written in es5 compatible syntax to make sure, that it can be executed on any environment and report using onUnsupported callback, when current browser can't even interpret the es2018 version of the bundle. In this case the application then runs in MPA mode

    Extending default script

    As mentioned before the runner script is fully extensible. For example to implement custom onUnsupported() and onError() callbacks, you'd do something like this:

    ./app/document/DocumentView.jsx
    <body>
    {/* ... */}
    <script>
    window.$IMA = window.$IMA || {};
    window.$IMA.$Runner = {
    /**
    * Optional onError handler. It is triggered in case the runtime
    * code fails to run the application.
    */
    onError: function (error) {
    throw new Error(error);
    },

    /**
    * Optional onUnsupported handler. It is triggered in case tests
    * for es and legacy version fails, which means that the APP runtime
    * code is never executed.
    */
    onUnsupported: function() {
    // Hit analytics with unsupported browser info
    },
    }
    </script>
    <script
    id='revivalSettings'
    dangerouslySetInnerHTML={{ __html: this.props.revivalSettings }}
    />
    </body>
    {/* ... */}

    Don't forget to apply the same changes to the spa.html, otherwise only SSR rendered pages will contain this override.

    note

    Since you will usually never want to change anything on the IMA.js Runner (apart from the previously mentioned callbacks), we won't go much deeper into it's the source code. You can always check it here and use it as a reference for custom overrides.

    However simply keeping your @ima/* dependencies up to will make sure, that you always receive the latest version of the runner script, which may evolve overtime.

    GenerateRuntimePlugin

    Since the webpack runtime is unique for every built, usually quite small, have to be loaded synchronously (which can impact the page load performance) and you would need to manually handle loading it's source code to the DOM, we have created GenerateRuntimePlugin to solve these issues.

    This plugin takes care of automatically generating the runtime consisting of the IMA.js runner code and webpack runtime (for both client bundles), which is then injected directly into the SPA template or SSR rendered html page. This means that you really don't have to worry about the existence of IMA.js runtime (while it's good to know that it does exist), since the framework handles all the hard work for you.

    It also comes with some performance benefits, since inlining these small scripts directly into the HTML removes the need to load additional 2 scripts synchronously, after browser parses the initial DOM.

    Dev server

    When you run your app using npx ima dev command, apart from building your application in development mode with HMR and all other dev features enabled, the CLI also starts companion express server - the dev server.

    By default it runs on http://localhost:3101 (this can be customized through ima.config.js or CLI options) and defines middlewares that are used mainly by the @ima/error-overlay (to properly display parsed error code snippets). Additionally it uses webpack-hot-middleware and webpack-dev-middleware to enable support for HMR.

    Using separate small server to host these middleware doesn't force us to define them directly on the app server, which could essentially produce some errors in certain situations.

    note

    Usually you don't have to think about the dev server during development and can simply pretend that it doesn't exist, since it is handled entirely by the CLI scripts. You can always have a look at the source code to learn more.

    + \ No newline at end of file diff --git a/cli/advanced-features/index.html b/cli/advanced-features/index.html index ded081c56..8fd9da6e2 100644 --- a/cli/advanced-features/index.html +++ b/cli/advanced-features/index.html @@ -4,13 +4,13 @@ Advanced Features | IMA.js - +
    -

    Error-overlay

    filesystem cache

    polyfill.js

    GenerateRunnerPlugin

    dynamic imports

    dev server

    - +

    Error-overlay

    filesystem cache

    polyfill.js

    GenerateRunnerPlugin

    dynamic imports

    dev server

    + \ No newline at end of file diff --git a/cli/cli-plugins-api/index.html b/cli/cli-plugins-api/index.html index 1452ac3cb..4f55eb9fb 100644 --- a/cli/cli-plugins-api/index.html +++ b/cli/cli-plugins-api/index.html @@ -4,13 +4,13 @@ CLI Plugins API | IMA.js - +
    -

    CLI Plugins API

    The CLI comes with built-in support for plugins. Plugins are used to extend or modify existing webpack config very easily or even run some pre/post processing scripts during the build process.

    The CLI plugin is usually a class or an object implementing ImaCliPlugin interface. This instance is then added to the plugins array field in the ima.config.js, which registers the plugin to the build process. Additionally to extending the webpack config, you have ability to provide additional custom CLI arguments.

    CLI Plugins API

    Each plugin has to comply with the following interface. Even though almost v everything method is not required and marked as optional, your plugin should implement at least one of the following methods in order to be of any use. Otherwise it would still work but the plugin would not do anything.

    /**
    * Interface for ima/cli plugins that can be defined in plugins field in ima.conf.js. These can be used
    * to extend functionality of default CLI with custom cli arguments and webpack config overrides.
    */
    export interface ImaCliPlugin {
    /**
    * Plugin name, used mainly for better debugging messages.
    */
    readonly name: string;

    /**
    * Optional additional CLI arguments to extend the set of existing ones.
    */
    readonly cliArgs?: Partial<Record<ImaCliCommand, CommandBuilder>>;

    /**
    * Optional plugin hook to do some pre processing right after the cli args are processed
    * and the imaConfig is loaded, before the webpack config creation and compiler run.
    */
    preProcess?(args: ImaCliArgs, imaConfig: ImaConfig): Promise<void>;

    /**
    * Called right before creating webpack configurations after preProcess call.
    * This hook lets you customize configuration contexts for each webpack config
    * that will be generated. This is usefull when you need to overrite configuration
    * contexts for values that are not editable anywhere else (like output folders).
    */
    prepareConfigurations?(
    configurations: ImaConfigurationContext[],
    imaConfig: ImaConfig,
    args: ImaCliArgs
    ): Promise<ImaConfigurationContext[]>;

    /**
    * Webpack callback function used by plugins to customize/extend ima webpack config before it's run.
    */
    webpack?(
    config: Configuration,
    ctx: ImaConfigurationContext,
    imaConfig: ImaConfig
    ): Promise<Configuration>;

    /**
    * Optional plugin hook to do some custom processing after the compilation has finished.
    * Attention! This hook runs only for build command.
    */
    postProcess?(args: ImaCliArgs, imaConfig: ImaConfig): Promise<void>;
    }

    Creating a CLI plugin

    In this section we're going to create custom plugin, which generates assets manifest json file. To achieve this we'll use WebpackManifestPlugin and extend our webpack config. We'll also define some additional CLI arguments that will enable us to overwrite certain settings on demand.

    First we're going to install the webpack-manifest-plugin:

    npm install webpack-manifest-plugin -D

    Then we need to define base class for our new CLI plugin. To make things easier we're going to work directly in the ima.config.js but in reality you'd be better of creating separate npm package for easier sharing between multiple IMA.js projects.

    ./ima.config.js
    class CliManifestPlugin {
    name = 'CliManifestPlugin';

    webpack(config, ctx, imaConfig) {}
    }

    module.exports = {
    plugins: [new CliManifestPlugin()],
    };

    Extending the webpack config

    Now we're going to initialize our manifest plugin. But we only want to do this when we are building the final bundle using the build command. For that we can use the ctx: ImaContext variable, which contains multiple flags and values describing current build context. One of those values is ctx.command which can be either dev or build.

    We are also going to make sure that we can provide options to our CLI plugin that are in this case passed directly to the webpack plugin.

    ./ima.config.js
    const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

    class CliManifestPlugin {
    //...
    #options = {};

    constructor(options) {
    this.#options = options;
    }

    webpack(config, ctx, imaConfig) {
    if (ctx.command === 'build') {
    config.plugins.push(new WebpackManifestPlugin(this.#options));
    }

    return config;
    }
    //...
    }
    tip

    Feel free to print the ctx object into the console and examine it's properties.

    Similarly to the ctx you can also use the imaConfig parameter, which contains loaded ima.config.js file. You can use this feature to have some additional custom plugin-specific definitions in the ima.config.js file too, or use existing settings for some additional functionality.

    We're now going to use these options and pass seed argument to the plugin. The seed object is used to share data between multiple manifest plugin instances (in our case multiple webpack compilations). This makes sure that the final manifest.json file contains paths to all generated assets and is not overwritten by each finished webpack compilation.

    ./ima.config.js
    //...
    const manifestSeed = {};

    module.exports = {
    plugins: [new CliManifestPlugin({ seed: manifestSeed })],
    };

    Custom CLI arguments

    There may be times you'd like to customize or enable/disable certain features on demand using CLI arguments. To demonstrate this we're going to define manifestBasePath CIL argument which will overwrite the basePath plugin option.

    You can define CLI arguments for each command separately, in our case, since the plugin does something only in build command, we're gonna do the same for the CLI arguments:

    ./ima.config.js
    class CliManifestPlugin {
    //...
    cliArgs = {
    dev: undefined, // Dev args will go here
    build: {
    manifestBasePath: {
    desc: 'Overwrite basePath default value',
    type: 'string',
    },
    },
    };
    //...
    }

    The argument definition is passed directly to the yargs parser, so anything that yargs options accept can be passed here. If you've done everything correctly you should even see the new argument in the command --help option:

    npx ima build --help

    ima build

    Build an application for production

    Options:
    --manifestBasePath Overwrite basePath default value [string]

    Accessing CLI arguments

    CLI argument values are merged into the ctx parameter, so you can access them here. In our case we would like to extend the plugin options with the CLI override:

    ./ima.config.js
    const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

    class CliManifestPlugin {
    //...
    webpack(config, ctx, imaConfig) {
    if (ctx.command === 'build') {
    config.plugins.push(
    new WebpackManifestPlugin({
    ...this.#options,
    basePath: ctx.manifestBasePath ?? '',
    })
    );
    }

    return config;
    }
    //...
    }

    Running npx ima build --manifestBasePath=path/prefix should be reflected in the generated manifest.json file in the ./build directory.

    Final results

    Below is the entire content of the ima.config.js file we've been building so far that you can use as a reference.

    ./ima.config.js
    const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

    class CliManifestPlugin {
    #options = {};

    name = 'CliManifestPlugin';

    cliArgs = {
    dev: undefined, // Dev args will go here
    build: {
    manifestBasePath: {
    desc: 'Overwrite basePath default value',
    type: 'string',
    },
    },
    };

    constructor(options) {
    this.#options = options;
    }

    webpack(config, ctx, imaConfig) {
    if (ctx.command === 'build') {
    config.plugins.push(
    new WebpackManifestPlugin({
    ...this.#options,
    basePath: ctx.manifestBasePath ?? '',
    })
    );
    }

    return config;
    }
    }

    const manifestSeed = {};

    module.exports = {
    plugins: [new CliManifestPlugin({ seed: manifestSeed })],
    };

    Using TypeScript

    Since the @ima/cli is written in TypeScript, there are TypeScript definitions you can use while defining your plugin. All types and interfaces are available as exports from the @ima/cli package while you can always have a look at our existing plugins, which are also written in TypeScript for an inspiration.

    Existing CLI plugins

    Currently we maintain 3 distinct CLI plugins that we actively use in our applications. These enables us to extend the feature set of the IMA.js CLI with additional functionality, which is not really suited to be available by default in the original CLI config, since their use is very situational. However you can almost certainly benefit from using these in your application.

    Most of these plugins also provide additional functionality that can be used outside of the CLI plugin definition, but it is essential for it to work properly.

    - +

    CLI Plugins API

    The CLI comes with built-in support for plugins. Plugins are used to extend or modify existing webpack config very easily or even run some pre/post processing scripts during the build process.

    The CLI plugin is usually a class or an object implementing ImaCliPlugin interface. This instance is then added to the plugins array field in the ima.config.js, which registers the plugin to the build process. Additionally to extending the webpack config, you have ability to provide additional custom CLI arguments.

    CLI Plugins API

    Each plugin has to comply with the following interface. Even though almost v everything method is not required and marked as optional, your plugin should implement at least one of the following methods in order to be of any use. Otherwise it would still work but the plugin would not do anything.

    /**
    * Interface for ima/cli plugins that can be defined in plugins field in ima.conf.js. These can be used
    * to extend functionality of default CLI with custom cli arguments and webpack config overrides.
    */
    export interface ImaCliPlugin {
    /**
    * Plugin name, used mainly for better debugging messages.
    */
    readonly name: string;

    /**
    * Optional additional CLI arguments to extend the set of existing ones.
    */
    readonly cliArgs?: Partial<Record<ImaCliCommand, CommandBuilder>>;

    /**
    * Optional plugin hook to do some pre processing right after the cli args are processed
    * and the imaConfig is loaded, before the webpack config creation and compiler run.
    */
    preProcess?(args: ImaCliArgs, imaConfig: ImaConfig): Promise<void>;

    /**
    * Called right before creating webpack configurations after preProcess call.
    * This hook lets you customize configuration contexts for each webpack config
    * that will be generated. This is usefull when you need to overrite configuration
    * contexts for values that are not editable anywhere else (like output folders).
    */
    prepareConfigurations?(
    configurations: ImaConfigurationContext[],
    imaConfig: ImaConfig,
    args: ImaCliArgs
    ): Promise<ImaConfigurationContext[]>;

    /**
    * Webpack callback function used by plugins to customize/extend ima webpack config before it's run.
    */
    webpack?(
    config: Configuration,
    ctx: ImaConfigurationContext,
    imaConfig: ImaConfig
    ): Promise<Configuration>;

    /**
    * Optional plugin hook to do some custom processing after the compilation has finished.
    * Attention! This hook runs only for build command.
    */
    postProcess?(args: ImaCliArgs, imaConfig: ImaConfig): Promise<void>;
    }

    Creating a CLI plugin

    In this section we're going to create custom plugin, which generates assets manifest json file. To achieve this we'll use WebpackManifestPlugin and extend our webpack config. We'll also define some additional CLI arguments that will enable us to overwrite certain settings on demand.

    First we're going to install the webpack-manifest-plugin:

    npm install webpack-manifest-plugin -D

    Then we need to define base class for our new CLI plugin. To make things easier we're going to work directly in the ima.config.js but in reality you'd be better of creating separate npm package for easier sharing between multiple IMA.js projects.

    ./ima.config.js
    class CliManifestPlugin {
    name = 'CliManifestPlugin';

    webpack(config, ctx, imaConfig) {}
    }

    module.exports = {
    plugins: [new CliManifestPlugin()],
    };

    Extending the webpack config

    Now we're going to initialize our manifest plugin. But we only want to do this when we are building the final bundle using the build command. For that we can use the ctx: ImaContext variable, which contains multiple flags and values describing current build context. One of those values is ctx.command which can be either dev or build.

    We are also going to make sure that we can provide options to our CLI plugin that are in this case passed directly to the webpack plugin.

    ./ima.config.js
    const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

    class CliManifestPlugin {
    //...
    #options = {};

    constructor(options) {
    this.#options = options;
    }

    webpack(config, ctx, imaConfig) {
    if (ctx.command === 'build') {
    config.plugins.push(new WebpackManifestPlugin(this.#options));
    }

    return config;
    }
    //...
    }
    tip

    Feel free to print the ctx object into the console and examine it's properties.

    Similarly to the ctx you can also use the imaConfig parameter, which contains loaded ima.config.js file. You can use this feature to have some additional custom plugin-specific definitions in the ima.config.js file too, or use existing settings for some additional functionality.

    We're now going to use these options and pass seed argument to the plugin. The seed object is used to share data between multiple manifest plugin instances (in our case multiple webpack compilations). This makes sure that the final manifest.json file contains paths to all generated assets and is not overwritten by each finished webpack compilation.

    ./ima.config.js
    //...
    const manifestSeed = {};

    module.exports = {
    plugins: [new CliManifestPlugin({ seed: manifestSeed })],
    };

    Custom CLI arguments

    There may be times you'd like to customize or enable/disable certain features on demand using CLI arguments. To demonstrate this we're going to define manifestBasePath CIL argument which will overwrite the basePath plugin option.

    You can define CLI arguments for each command separately, in our case, since the plugin does something only in build command, we're gonna do the same for the CLI arguments:

    ./ima.config.js
    class CliManifestPlugin {
    //...
    cliArgs = {
    dev: undefined, // Dev args will go here
    build: {
    manifestBasePath: {
    desc: 'Overwrite basePath default value',
    type: 'string',
    },
    },
    };
    //...
    }

    The argument definition is passed directly to the yargs parser, so anything that yargs options accept can be passed here. If you've done everything correctly you should even see the new argument in the command --help option:

    npx ima build --help

    ima build

    Build an application for production

    Options:
    --manifestBasePath Overwrite basePath default value [string]

    Accessing CLI arguments

    CLI argument values are merged into the ctx parameter, so you can access them here. In our case we would like to extend the plugin options with the CLI override:

    ./ima.config.js
    const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

    class CliManifestPlugin {
    //...
    webpack(config, ctx, imaConfig) {
    if (ctx.command === 'build') {
    config.plugins.push(
    new WebpackManifestPlugin({
    ...this.#options,
    basePath: ctx.manifestBasePath ?? '',
    })
    );
    }

    return config;
    }
    //...
    }

    Running npx ima build --manifestBasePath=path/prefix should be reflected in the generated manifest.json file in the ./build directory.

    Final results

    Below is the entire content of the ima.config.js file we've been building so far that you can use as a reference.

    ./ima.config.js
    const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

    class CliManifestPlugin {
    #options = {};

    name = 'CliManifestPlugin';

    cliArgs = {
    dev: undefined, // Dev args will go here
    build: {
    manifestBasePath: {
    desc: 'Overwrite basePath default value',
    type: 'string',
    },
    },
    };

    constructor(options) {
    this.#options = options;
    }

    webpack(config, ctx, imaConfig) {
    if (ctx.command === 'build') {
    config.plugins.push(
    new WebpackManifestPlugin({
    ...this.#options,
    basePath: ctx.manifestBasePath ?? '',
    })
    );
    }

    return config;
    }
    }

    const manifestSeed = {};

    module.exports = {
    plugins: [new CliManifestPlugin({ seed: manifestSeed })],
    };

    Using TypeScript

    Since the @ima/cli is written in TypeScript, there are TypeScript definitions you can use while defining your plugin. All types and interfaces are available as exports from the @ima/cli package while you can always have a look at our existing plugins, which are also written in TypeScript for an inspiration.

    Existing CLI plugins

    Currently we maintain 3 distinct CLI plugins that we actively use in our applications. These enables us to extend the feature set of the IMA.js CLI with additional functionality, which is not really suited to be available by default in the original CLI config, since their use is very situational. However you can almost certainly benefit from using these in your application.

    Most of these plugins also provide additional functionality that can be used outside of the CLI plugin definition, but it is essential for it to work properly.

    + \ No newline at end of file diff --git a/cli/compiler-features/index.html b/cli/compiler-features/index.html index 03cb98a8a..560cee45a 100644 --- a/cli/compiler-features/index.html +++ b/cli/compiler-features/index.html @@ -4,13 +4,13 @@ Compiler features | IMA.js - +
    -

    Compiler features

    The IMA.js CLI uses webpack behind the scenes to compile, minify and run the application in dev mode. It comes pre-configured with some options, plugins and loaders, which are described in the following sections.

    Server and client bundles

    The CLI creates 3 separate bundles (2 in dev mode for performance reasons) with their own configurations. One server bundle (used in express for SSR) and two client bundles - client and client.es, where one targets the es2018 and the other es2022 version of the javascript.

    This can be further customized using the disableLegacyBuilt option in ima.config.js.

    tip

    To make the CLI build both es versions in dev mode, run it with npx ima dev --legacy option.

    Keep in mind that hot module replacement (HMR) is configured to work only with the latest es version (manual browser reload is required to see any changes on the legacy version).

    Filesystem Cache

    The webpack filesystem cache feature is enabled by default to improve consecutive build times in development and production mode.

    The CLI automatically generates cache key based on used set of CLI options, which somehow affect the produced output. Not all options affect cache key generation, however you may notice that sometimes the build speeds can behave as if there is no filesystem cache. To see which options affect the cache key generation, take a look at the createCacheKey() function.

    note

    Note that each command and bundle maintains it's own set of coexisting cache. To clear the cache, use --clearCache option in build or dev commands.

    JavaScript/React

    To bundle JS files we opted to use swc, a Rust-based JavaScript compiler. This decision was based on our results from testing and measuring build times, where we saw 2-3 times the speed benefit (depending on the application size) of using swc over babel compiler.

    By default the application compiles both, the application files (sourced from ./app folder) and vendor files (sourced from ./node_modules directory) to make sure that it can run in targeted environments without any issues.

    The swc compiler is configured to leverage the power of "env" functionality (preset-env in babel), in combination with core-js to automatically polyfill missing APIs that are used throughout the codebase, that the targeted environment doesn't support.

    This configuration can be easily customized using swc option in ima.config.js.

    note

    This means that you can write your code using the latest and greatest from the ECMAscript language and the swc makes sure to compile these features down to the latest supported syntax or automatically inject core-js polyfills.

    danger

    Keep in mind that overuse of these may result in larger JS bundles due to the need to inject more core-js polyfills. Also browser APIs like for example AbortController or fetch are not handled by the core-js and must be included manually. See polyfills in advanced features section.

    React

    In dev we use the development version of react library (for better debugging) and react-refresh for HMR. This is switched to production for production builds. By default the compiler is configured to work with automatic JSX runtime, so there's no need to import react library at top of every jsx file. This can be changed to classic in ima.config.js.

    Typescript

    From IMA.js v18 we've introduced support for Typescript in your application code. To enable it, simply install typescript dependency and create tsconfig.json file in the root of your project.

    tip

    For more information and additional tips about TypeScript usage in IMA.js applications, see the TypeScript section.

    CSS/LESS

    There's built in support for CSS and LESS preprocessor. Both of these have the same featureset. To use any CSS you have to import the files anywhere in your application. These imports are then combined and extracted to single app.css file.

    tip

    ./app/main.js is a good place to use for global CSS files, since it is an entry point to IMA.js application and these imports will be included at top of the built app.css file.

    CSS Modules

    Both loaders fully support CSS Modules for files with *.modules.css or *.modules.less postfixes, with local as default scoping.

    ./app/page/home/home.module.less
    :global {
    :root: {
    --bg-color: #fff;
    }
    }

    .home {
    background: var(--bg-color);
    }
    ./app/page/home/Home.jsx
    import styles from './home.module.less';

    function Home() {
    return
    <div className={styles.home}>HomePage</div>
    );
    }

    globals.less

    This file is located at ./app/less/globals.less and it is automatically imported on top of every other processed LESS file. It allows you to easily share globals across less files.

    tip

    Use this file to import other mixins and global variables which are then available in all other *.less files automatically, without the need to import them explicitly.

    ./app/less/globals.less
    @import './mixins/*.less';

    @global-red: red;
    ./app/less/app.less
    body {
    // No direct import of 'globals.less' is needed.
    background: @global-red;
    }

    Glob less imports

    The less-loader uses less-plugin-glob by default in it's configuration. This means that glob imports are fully supported.

    ./app/less/app.less
    @import './mixins/*.less';

    /* Non-relative imports are resolved through node resolver. */
    @import "@ima/**/atoms/**/*.less";
    @import "@ima/**/molecules/**/*.less";
    @import "@ima/**/organisms/**/*.less";

    PostCSS

    IMA.js has built-in support for PostCSS during CSS/LESS compilation.

    Out of the box without any additional configuration, it comes pre-configured with following plugins:

    1. postcss-flexbugs-fixes - tries to fix all known flexbox issues.
    2. postcss-preset-env - converts modern CSS back into something the old browsers can understand (back to IE11). It comes with: autoprefixer, stage 3 and custom-properties: false features.

    All these features can be easily customized using postcss option in ima.config.js.

    Assets

    All other assets are either inlined as base64 encoded string or copied to the ./build/static/media folder, where default import represents assets's public URL.

    Images

    Images (bmp, gif, jpeg, png, webp, svg) are automatically inlined if their size is below imageInlineSizeLimit, which can be configured in ima.config.js, with default value of 8Kb. Images exceeding this size limit are copied to the static media folder and import return's their public URL.

    To enforce either one of the two modes, you can use ?external or ?inline query parameter in the import path:

    // This always converts the image to base64 encoded string and inlines it.
    import InlineImage from './image.png?inline';

    // This always returns image public URL, no matter it's size
    import ImageURL from './image.png?external';

    Text files

    When you import one of these text files - csv, txt, html, you receive their contents. Similarly to the images, you can enforce getting their public URL by using the ?external query parameter.

    // Returns file contents in the default import
    import IndexSource from './index.html';

    // Returns the file public URL
    import IndexURL from './index.html?external';

    ./app/public folder

    Everything in this folder is copied to the ./build/static/public and available through the express static files route (http://localhost:3001/static/public/).

    Compression

    When you built the application bundle, all static assets are additionally compressed using brotli and gzip compression (with .br and .gz extensions respectively). To customize this behavior, take a look at ima.config.js configuration section.

    Languages

    The language files are compile using messageformat library.

    - +

    Compiler features

    The IMA.js CLI uses webpack behind the scenes to compile, minify and run the application in dev mode. It comes pre-configured with some options, plugins and loaders, which are described in the following sections.

    Server and client bundles

    The CLI creates 3 separate bundles (2 in dev mode for performance reasons) with their own configurations. One server bundle (used in express for SSR) and two client bundles - client and client.es, where one targets the es2018 and the other es2022 version of the javascript.

    This can be further customized using the disableLegacyBuilt option in ima.config.js.

    tip

    To make the CLI build both es versions in dev mode, run it with npx ima dev --legacy option.

    Keep in mind that hot module replacement (HMR) is configured to work only with the latest es version (manual browser reload is required to see any changes on the legacy version).

    Filesystem Cache

    The webpack filesystem cache feature is enabled by default to improve consecutive build times in development and production mode.

    The CLI automatically generates cache key based on used set of CLI options, which somehow affect the produced output. Not all options affect cache key generation, however you may notice that sometimes the build speeds can behave as if there is no filesystem cache. To see which options affect the cache key generation, take a look at the createCacheKey() function.

    note

    Note that each command and bundle maintains it's own set of coexisting cache. To clear the cache, use --clearCache option in build or dev commands.

    JavaScript/React

    To bundle JS files we opted to use swc, a Rust-based JavaScript compiler. This decision was based on our results from testing and measuring build times, where we saw 2-3 times the speed benefit (depending on the application size) of using swc over babel compiler.

    By default the application compiles both, the application files (sourced from ./app folder) and vendor files (sourced from ./node_modules directory) to make sure that it can run in targeted environments without any issues.

    The swc compiler is configured to leverage the power of "env" functionality (preset-env in babel), in combination with core-js to automatically polyfill missing APIs that are used throughout the codebase, that the targeted environment doesn't support.

    This configuration can be easily customized using swc option in ima.config.js.

    note

    This means that you can write your code using the latest and greatest from the ECMAscript language and the swc makes sure to compile these features down to the latest supported syntax or automatically inject core-js polyfills.

    danger

    Keep in mind that overuse of these may result in larger JS bundles due to the need to inject more core-js polyfills. Also browser APIs like for example AbortController or fetch are not handled by the core-js and must be included manually. See polyfills in advanced features section.

    React

    In dev we use the development version of react library (for better debugging) and react-refresh for HMR. This is switched to production for production builds. By default the compiler is configured to work with automatic JSX runtime, so there's no need to import react library at top of every jsx file. This can be changed to classic in ima.config.js.

    Typescript

    From IMA.js v18 we've introduced support for Typescript in your application code. To enable it, simply install typescript dependency and create tsconfig.json file in the root of your project.

    tip

    For more information and additional tips about TypeScript usage in IMA.js applications, see the TypeScript section.

    CSS/LESS

    There's built in support for CSS and LESS preprocessor. Both of these have the same featureset. To use any CSS you have to import the files anywhere in your application. These imports are then combined and extracted to single app.css file.

    tip

    ./app/main.js is a good place to use for global CSS files, since it is an entry point to IMA.js application and these imports will be included at top of the built app.css file.

    CSS Modules

    Both loaders fully support CSS Modules for files with *.modules.css or *.modules.less postfixes, with local as default scoping.

    ./app/page/home/home.module.less
    :global {
    :root: {
    --bg-color: #fff;
    }
    }

    .home {
    background: var(--bg-color);
    }
    ./app/page/home/Home.jsx
    import styles from './home.module.less';

    function Home() {
    return
    <div className={styles.home}>HomePage</div>
    );
    }

    globals.less

    This file is located at ./app/less/globals.less and it is automatically imported on top of every other processed LESS file. It allows you to easily share globals across less files.

    tip

    Use this file to import other mixins and global variables which are then available in all other *.less files automatically, without the need to import them explicitly.

    ./app/less/globals.less
    @import './mixins/*.less';

    @global-red: red;
    ./app/less/app.less
    body {
    // No direct import of 'globals.less' is needed.
    background: @global-red;
    }

    Glob less imports

    The less-loader uses less-plugin-glob by default in it's configuration. This means that glob imports are fully supported.

    ./app/less/app.less
    @import './mixins/*.less';

    /* Non-relative imports are resolved through node resolver. */
    @import "@ima/**/atoms/**/*.less";
    @import "@ima/**/molecules/**/*.less";
    @import "@ima/**/organisms/**/*.less";

    PostCSS

    IMA.js has built-in support for PostCSS during CSS/LESS compilation.

    Out of the box without any additional configuration, it comes pre-configured with following plugins:

    1. postcss-flexbugs-fixes - tries to fix all known flexbox issues.
    2. postcss-preset-env - converts modern CSS back into something the old browsers can understand (back to IE11). It comes with: autoprefixer, stage 3 and custom-properties: false features.

    All these features can be easily customized using postcss option in ima.config.js.

    Assets

    All other assets are either inlined as base64 encoded string or copied to the ./build/static/media folder, where default import represents assets's public URL.

    Images

    Images (bmp, gif, jpeg, png, webp, svg) are automatically inlined if their size is below imageInlineSizeLimit, which can be configured in ima.config.js, with default value of 8Kb. Images exceeding this size limit are copied to the static media folder and import return's their public URL.

    To enforce either one of the two modes, you can use ?external or ?inline query parameter in the import path:

    // This always converts the image to base64 encoded string and inlines it.
    import InlineImage from './image.png?inline';

    // This always returns image public URL, no matter it's size
    import ImageURL from './image.png?external';

    Text files

    When you import one of these text files - csv, txt, html, you receive their contents. Similarly to the images, you can enforce getting their public URL by using the ?external query parameter.

    // Returns file contents in the default import
    import IndexSource from './index.html';

    // Returns the file public URL
    import IndexURL from './index.html?external';

    ./app/public folder

    Everything in this folder is copied to the ./build/static/public and available through the express static files route (http://localhost:3001/static/public/).

    Compression

    When you built the application bundle, all static assets are additionally compressed using brotli and gzip compression (with .br and .gz extensions respectively). To customize this behavior, take a look at ima.config.js configuration section.

    Languages

    The language files are compile using messageformat library.

    + \ No newline at end of file diff --git a/cli/ima-config-js/index.html b/cli/ima-config-js/index.html index f0b7e50f7..850a14889 100644 --- a/cli/ima-config-js/index.html +++ b/cli/ima-config-js/index.html @@ -4,14 +4,14 @@ ima.config.js | IMA.js - +

    ima.config.js

    To additionally customize the build configuration of IMA.js, you can create a ima.config.js file in the root of your project (next to package.json).

    ima.config.js is regular JavaScript module that is loaded during the build configuration initialization (in Node.js environment) and it is not included in the final application bundle.

    An example of ima.config.js file can look something like this:

    ./ima.config.js
    const postcssUnmq = require('postcss-unmq');
    const { AnalyzePlugin } = require('@ima/cli');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    publicPath: '/public/',
    watchOptions: {
    ignored: /(node_modules|build|.husky|_templates|.git)\/(?!@ima).*/
    },
    plugins: [
    new AnalyzePlugin()
    ],
    webpack: async (config, ctx) => {
    // Enable webpack infrastructure logging
    if (ctx.command === 'dev') {
    config.infrastructureLogging = {
    level: 'info',
    };
    }

    return config;
    },
    postcss: (config, ctx) => {
    config.postcssOptions.plugins.push(postcssUnmq({ width: 540 }));

    return config;
    },
    languages: {
    cs: [
    './node_modules/@ima/**/*CS.json',
    './app/**/*CS.json'
    ],
    en: [
    './node_modules/@ima/**/*EN.json',
    './app/**/*EN.json'
    ]
    }
    };
    tip

    Use the @type jsdoc annotation to enable ts-types code completions.

    Configuration options

    The ima.config.js file should export an object with any combination of the following configuration options.

    webpack

    async function(config, ctx, imaConfig): config

    This is the most advanced and versatile configuration option, allowing you to change the generated webpack configuration directly before it's passed to the compiler. This function is executed last in the whole configuration pipeline, meaning that all default configurations and CLI plugin configurations are already merged into the config value. This allows you to completely customize the final config form.

    The function receives 3 arguments and has to always return (mutated) config object:

    • config - webpack configuration object (just like the one you usually define in webpack.config.js).
    • ctx - current configuration context. As we mentioned in the compiler features, the app builds 3 different bundles. Using ctx.name you can find out which configuration you are currently editing. There are many additional values that help you identify current build state. You can use these to further customize the config only in some cases or for certain bundles. For more information take a look at the argument type.
    • imaConfig - loaded ima.config.js file, with defaults.

    The following example turns on minification for server bundle for build command:

    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    webpack: async (config, ctx) => {
    if (ctx.command === 'build' && ctx.name === 'server') {
    config.optimization.minimize = true;
    }

    return config;
    },
    };

    :::Note

    Since this function can be implemented in ima.config.js and also CLI plugins, the resolve order is following default @ima/cli config -> CLI plugin configs -> ima.config.js.

    :::

    swc

    async function(swcLoaderOptions, ctx): swcLoaderOptions

    Similarly to webpack, this function is executed with the swc-loader default options and it's result is then passed to the loader itself. This allows you to customize the swc compiler options in easier and more direct way than you'd have to do when using the webpack option.

    For example, to enable support for the ECMAScript proposals core-js feature, you would do the following:

    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    swc: async (swcLoaderOptions, ctx) => {
    swcLoaderOptions.env.shippedProposals = true;

    return swcLoaderOptions;
    },
    };

    swcVendor

    async function(swcLoaderOptions, ctx): swcLoaderOptions

    Works same as the aforementioned swc options, except this config is applied to vendor files that match regular expressions defined in the transformVendorPaths settings.

    postcss

    async function(postCssLoaderOptions, ctx): postCssLoaderOptions

    Lastly, this function is used to customize the postcss-loader options. Among the rest you can easily define custom PostCSS plugins or completely overwrite the default set:

    ./ima.config.js
    const postcssUnmq = require('postcss-unmq');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    swc: async (postCssLoaderOptions, ctx) => {
    postCssLoaderOptions.postcssOptions.plugins.push(
    postcssUnmq({ width: 540 })
    );

    return postCssLoaderOptions;
    },
    };
    info

    The webpack configuration intentionally ignores any .postcssrc configuration files to prevent potential conflicts with multiple config files.

    prepareConfigurations

    async function(configurations: ImaConfigurationContext[], imaConfig: ImaConfig, args: ImaCliArgs): Promise<ImaConfigurationContext[]>

    Called right before creating webpack configurations after preProcess call. This hook lets you customize configuration contexts for each webpack config that will be generated. This allows you to override values of context variables like useHMR, useTypeScript which define -how the final webpack config is generated, without the need to customize the config itself.

    languages

    object

    i18n language files configuration. The language option expects an object with key/value pairs, where key represents the language and value an array of messageformat compliant JSON files. For more information about localization, see the dictionary section.

    note

    Globs are fully supported and resolved through globby npm package.

    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    languages: {
    cs: [
    './node_modules/@ima/**/*CS.json',
    './node_modules/@cns/**/*CS.json',
    './app/**/*CS.json'
    ],
    en: [
    './node_modules/@ima/**/*EN.json',
    './node_modules/@cns/**/*EN.json',
    './app/**/*EN.json'
    ]
    }
    }

    jsxRuntime

    'classic' | 'automatic' = 'automatic'

    Use jsxRuntime option to enable classic or automatic mode for jsx transformations. For more information see Introducing the New JSX Transform.

    webpackAliases

    object

    The webpackAliases options is passed directly to the webpack resolve.alias configuration. You can use this to define additional path aliases to the already existing app/* alias, which points to the ./app directory.

    To have the ability to use absolute paths, which are resolved from the ./app/components and ./app/pages directory:

    import Home from 'components/home/Home';
    import DetailView from 'pages/detail/DetailView';

    The webpackAliases option configuration could look something like this:

    ./ima.config.js
    const path = require('path');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    webpackAliases: {
    'components': path.resolve('./app/components'),
    'pages': path.resolve('./app/pages'),
    };
    };

    sourceMaps

    boolean | string = false

    The sourceMaps option enables source maps in the production build. Use true for 'source-map' or any other string value compatible with webpack devtool option.

    devServer

    object

    Similarly to the CLI options, you can use the devServer option to override defaults for our companion dev server.

    The only thing that's configurable through the ima.config.js only, is the writeToDiskFilter function. This allows you to force the dev server to write certain files to disk, even if you're serving them from memory in the watch mode.

    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    devServer: {
    port: 3101,
    hostname: 'localhost',
    publicUrl: 'http://localhost:3101',
    writeToDiskFilter: (filePath) => false,
    // Use to define custom origin for CORS headers on dev server
    origin: 'http://localhost:3001',
    };
    };
    note

    The CLI arguments always take precedence over any other configurations.

    publicPath

    string = '/'

    The publicPath option is used to specify base path for all assets within the application. (see more at webpack public path).

    tip

    Use this option to change base public path for static served files, for example when uploading static files to CDN.

    Runtime public path

    When you want to change public path during runtime, you can use IMA_PUBLIC_PATH env variable, when starting the application server. This variable takes preference before the ima config publicPath option.

    IMA_PUBLIC_PATH="https://cdn.basePath/cd_F/" node ./server/server.js

    Additionally, when used, runner.js injected scripts and styles have defined fallback to local static files, in case the runtime public path assets are not available (CDN is down for example).

    Default settings

    If you want to serve your static files on a different route (default is /static), customize the staticPath option in ./server/config/environment.js file and change publicPath option accordingly:

    ./server/config/environment.js
    module.exports = (() => {
    return {
    prod: {
    $Server: {
    /**
    * The built static files are served on the
    * http://localhost:3001/pro/static base url
    */
    staticPath: '/pro/static',
    }
    }
    }
    });
    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    publicPath: '/pro/static/',
    };

    compress

    boolean = true

    Enables brotli and gzip compression for production assets (in build command). Set to false to disable this feature.

    imageInlineSizeLimit

    number = 8192

    The imageInlineSizeLimit configuration option is already described in the compiler features section. Essentially it's a image size threshold for it's automatic inlining as a base64 string.

    disableLegacyBuild

    boolean

    Set to true to disable building of the client bundle (older ECMAScript target).

    Don't forget to remove script sources from the $Source option in app environment config

    caution

    The application will now only execute the modern version of the client bundle (client.es), meaning that the it will only work on the latest versions of modern browsers.

    This can be useful if you're building an app, where you are able to set constrains for the supported browsers (e.g. internal admin page).

    transformVendorPaths

    { include?: RegExp[]; exclude?: RegExp[]; }

    caution

    This is an advanced feature.

    Using this option you can include/exclude array of regular expressions that are matched against file paths of processed vendor files (= imported files from node_modules). These files are then processed through swc-loader that makes sure to compile their syntax to currently supported target (ES9). This transformation is executed only for the legacy client bundle.

    By default the CLI always matches all files under the @ima namespace, since we release our plugins in latest ECMA syntax and they need to be compiled down to older syntaxes with proper core-js polyfills.

    tip

    If you use any 3rd party libraries that you are not sure if they support your currently supported browser environments, add their package names as regular expressions to this array and they will be compiled using swc-loader with proper polyfill injections from the core-js package.

    watchOptions

    object

    watchOptions is an object, passed to the webpack watch compiler. You can customize watchOptions.ignored files settings or watchOptions.aggregateTimeout if you have any issues with the default values.

    For more information visit the webpack documentation.

    tip

    If you have any issues with webpack compilation launching multiple times per one file save, try to increase the watchOptions.aggregateTimeout number and see if it helps.

    experiments

    object

    This is a place where you can enable IMA.js configuration experiments. Don't confuse this with the webpack experiments field, this is used only for our potential future configuration updates or changes, which may be enabled by default in the future (much like webpack does).

    Currently there's only one running experiment option experiments.css, that uses webpack native CSS support which completely replaces the css-loader and mini-css-extract-plugin libraries.

    plugins

    ImaCliPlugin[]

    Array of IMA.js CLI plugin instances. For more information about CLI plugins, see Plugins API section.

    - +how the final webpack config is generated, without the need to customize the config itself.

    languages

    object

    i18n language files configuration. The language option expects an object with key/value pairs, where key represents the language and value an array of messageformat compliant JSON files. For more information about localization, see the dictionary section.

    note

    Globs are fully supported and resolved through globby npm package.

    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    languages: {
    cs: [
    './node_modules/@ima/**/*CS.json',
    './node_modules/@cns/**/*CS.json',
    './app/**/*CS.json'
    ],
    en: [
    './node_modules/@ima/**/*EN.json',
    './node_modules/@cns/**/*EN.json',
    './app/**/*EN.json'
    ]
    }
    }

    jsxRuntime

    'classic' | 'automatic' = 'automatic'

    Use jsxRuntime option to enable classic or automatic mode for jsx transformations. For more information see Introducing the New JSX Transform.

    webpackAliases

    object

    The webpackAliases options is passed directly to the webpack resolve.alias configuration. You can use this to define additional path aliases to the already existing app/* alias, which points to the ./app directory.

    To have the ability to use absolute paths, which are resolved from the ./app/components and ./app/pages directory:

    import Home from 'components/home/Home';
    import DetailView from 'pages/detail/DetailView';

    The webpackAliases option configuration could look something like this:

    ./ima.config.js
    const path = require('path');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    webpackAliases: {
    'components': path.resolve('./app/components'),
    'pages': path.resolve('./app/pages'),
    };
    };

    sourceMaps

    boolean | string = false

    The sourceMaps option enables source maps in the production build. Use true for 'source-map' or any other string value compatible with webpack devtool option.

    devServer

    object

    Similarly to the CLI options, you can use the devServer option to override defaults for our companion dev server.

    The only thing that's configurable through the ima.config.js only, is the writeToDiskFilter function. This allows you to force the dev server to write certain files to disk, even if you're serving them from memory in the watch mode.

    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    devServer: {
    port: 3101,
    hostname: 'localhost',
    publicUrl: 'http://localhost:3101',
    writeToDiskFilter: (filePath) => false,
    // Use to define custom origin for CORS headers on dev server
    origin: 'http://localhost:3001',
    };
    };
    note

    The CLI arguments always take precedence over any other configurations.

    publicPath

    string = '/'

    The publicPath option is used to specify base path for all assets within the application. (see more at webpack public path).

    tip

    Use this option to change base public path for static served files, for example when uploading static files to CDN.

    Runtime public path

    When you want to change public path during runtime, you can use IMA_PUBLIC_PATH env variable, when starting the application server. This variable takes preference before the ima config publicPath option.

    IMA_PUBLIC_PATH="https://cdn.basePath/cd_F/" node ./server/server.js

    Additionally, when used, runner.js injected scripts and styles have defined fallback to local static files, in case the runtime public path assets are not available (CDN is down for example).

    Default settings

    If you want to serve your static files on a different route (default is /static), customize the staticPath option in ./server/config/environment.js file and change publicPath option accordingly:

    ./server/config/environment.js
    module.exports = (() => {
    return {
    prod: {
    $Server: {
    /**
    * The built static files are served on the
    * http://localhost:3001/pro/static base url
    */
    staticPath: '/pro/static',
    }
    }
    }
    });
    ./ima.config.js
    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    publicPath: '/pro/static/',
    };

    compress

    boolean = true

    Enables brotli and gzip compression for production assets (in build command). Set to false to disable this feature.

    imageInlineSizeLimit

    number = 8192

    The imageInlineSizeLimit configuration option is already described in the compiler features section. Essentially it's a image size threshold for it's automatic inlining as a base64 string.

    disableLegacyBuild

    boolean

    Set to true to disable building of the client bundle (older ECMAScript target).

    Don't forget to remove script sources from the $Source option in app environment config

    caution

    The application will now only execute the modern version of the client bundle (client.es), meaning that the it will only work on the latest versions of modern browsers.

    This can be useful if you're building an app, where you are able to set constrains for the supported browsers (e.g. internal admin page).

    transformVendorPaths

    { include?: RegExp[]; exclude?: RegExp[]; }

    caution

    This is an advanced feature.

    Using this option you can include/exclude array of regular expressions that are matched against file paths of processed vendor files (= imported files from node_modules). These files are then processed through swc-loader that makes sure to compile their syntax to currently supported target (ES9). This transformation is executed only for the legacy client bundle.

    By default the CLI always matches all files under the @ima namespace, since we release our plugins in latest ECMA syntax and they need to be compiled down to older syntaxes with proper core-js polyfills.

    tip

    If you use any 3rd party libraries that you are not sure if they support your currently supported browser environments, add their package names as regular expressions to this array and they will be compiled using swc-loader with proper polyfill injections from the core-js package.

    watchOptions

    object

    watchOptions is an object, passed to the webpack watch compiler. You can customize watchOptions.ignored files settings or watchOptions.aggregateTimeout if you have any issues with the default values.

    For more information visit the webpack documentation.

    tip

    If you have any issues with webpack compilation launching multiple times per one file save, try to increase the watchOptions.aggregateTimeout number and see if it helps.

    experiments

    object

    This is a place where you can enable IMA.js configuration experiments. Don't confuse this with the webpack experiments field, this is used only for our potential future configuration updates or changes, which may be enabled by default in the future (much like webpack does).

    Currently there's only one running experiment option experiments.css, that uses webpack native CSS support which completely replaces the css-loader and mini-css-extract-plugin libraries.

    plugins

    ImaCliPlugin[]

    Array of IMA.js CLI plugin instances. For more information about CLI plugins, see Plugins API section.

    + \ No newline at end of file diff --git a/cli/index.html b/cli/index.html index d5de925e3..c5441295d 100644 --- a/cli/index.html +++ b/cli/index.html @@ -4,13 +4,13 @@ Introduction to @ima/cli | IMA.js - +
    -

    Introduction to @ima/cli

    The IMA.js CLI allows you to build and watch your application for changes during development. These features are handle by the only two currently supported commands build and dev.

    You can always list available commands by running:

    npx ima --help
    note

    npx comes pre-installed with npm 5.2+ and higher.

    This should produce following output:

    Usage: ima <command>

    Commands:
    ima build Build an application for production
    ima dev Run application in development watch mode

    Options:
    --version Show version number [boolean]
    --help Show help [boolean]

    Development

    The npx ima dev command starts the application in the development mode with HMR, error-overlay, source maps and other debugging tools enabled.

    By default the application starts on http://localhost:3001 with companion dev server running at http://localhost:3101. These can be further customized through the app environment settings and CLI arguments.

    You can also run npx ima dev --help to list all available options that you can use:

    ima dev

    Run application in development watch mode

    Options:
    --version Show version number [boolean]
    --help Show help [boolean]
    --clean Clean build folder before building the application [boolean] [default: true]
    --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]
    --verbose Use default webpack CLI output instead of custom one [boolean]
    --inspect Enable Node inspector mode [boolean]
    --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]
    --open Opens browser window after server has been started [boolean] [default: true]
    --openUrl Custom URL used when opening browser window [string]
    --legacy Runs application in legacy mode [boolean] [default: false]
    --forceLegacy Forces runner.js to execute legacy client code [boolean] [default: false]
    --forceSPA Forces application to run in SPA mode [boolean] [default: false]
    --writeToDisk Write static files to disk, instead of serving it from memory [boolean] [default: false]
    --reactRefresh Enable/disable react fast refresh for React components [boolean] [default: true]
    --lazyServer Enable/disable lazy init of server app factory [boolean] [default: true]
    --port Dev server port (overrides ima.config.js settings) [number]
    --hostname Dev server hostname (overrides ima.config.js settings) [string]
    --publicUrl Dev server publicUrl (overrides ima.config.js settings) [string]
    info

    Any of the above mentioned options can be combined together in all different combinations and all options have specified default value. This means that in normal cases you can run npx ima dev without any additional arguments.

    Build

    Builds the application in production mode with all optimizations enabled (compression, minification, etc.). The build command drops some options compared to the dev command. While adding few build specific commands. npx build --help produces:

    ima build

    Build an application for production

    Options:
    --version Show version number [boolean]
    --help Show help [boolean]
    --clean Clean build folder before building the application [boolean] [default: true]
    --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]
    --verbose Use default webpack CLI output instead of custom one [boolean]
    --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]
    --profile Turn on profiling support in production [boolean] [default: false]

    CLI options

    Most of the following options are available for both dev and build commands, however some may be exclusive to only one of them. You can always use the --help argument to show all available options for each command.

    tip

    When you run into any issues with the application build, you can always run the app with npx ima dev --clearCache to make sure that all cache and tmp files are deleted before next build and see if this resolves your issues.

    Similarly you can use the --verbose option to show more information during build that can aid you in debugging process in case anything happens.

    --version

    Prints @ima/cli version.

    --help

    Prints help dialog.

    --clean

    boolean = true

    Deletes ./build folder before running the application.

    --clearCache

    boolean = false

    Clears ./node_modules/.cache folder. This is used to store webpack filesystem cache and other webpack loader and plugins cache.

    --verbose

    boolean = false

    Disables custom CLI logging style in favor of default webpack CLI verbose. This can be useful for debugging.

    --inspect

    boolean = false

    Disable/enable node inspector mode.

    --ignoreWarnings

    boolean = false

    Ignore reporting of webpack warning messages. The CLI automatically caches all existing warnings and shows just new warnings rebuilds in watch mode.

    --open

    boolean = true

    Enable/disable auto opening of app URL in the browser window on startup.

    tip

    If you find this option annoying, you can completely disable this feature across all IMA.js applications by putting IMA_CLI_OPEN=false in your environment.

    --openUrl

    boolean = true

    Allows you to customize URL which is opened when the server starts in development mode.

    tip

    You can also use IMA_CLI_OPEN_URL='http://ima.dev:3001' env variable to set this option.

    This is usefull when you have project-specific URLs. You can then set this environment variable in application's ima.config.js and don't have to worry about using --openUrl CLI argument everytime you're starting the application in dev mode.

    --legacy

    boolean = false

    By default the CLI only builds es version of JS files in development mode. Use this option to enable additional build of non es version.

    --forceLegacy

    boolean = false

    Enables legacy mode and forces runner.js to load legacy code even if targeted browser supports the latest client es version.

    --forceSPA

    boolean = false

    Forces the application to run in SPA mode.

    --profile

    boolean = false

    Disables some optimizations to allow for better debugging while also trying to be as close to the production build as possible. Currently this option disables mangling of classes and functions, which produces more readable stack traces.

    --writeToDisk

    boolean = false

    By default the app client static files are served from memory in dev mode. Using this option you can force webpack to write these files and serve them from the disk.

    tip

    This option can be useful in some cases where you need to take a look at the compile source code, where it's easier to browse these files locally, rather than on the static server.

    --reactRefresh

    boolean = true

    Disable/enable react fast refresh for React components.

    tip

    Disable this option if you are watching and editing node_modules files, this may result in less performant but more stable HMR experience.

    --lazyServer

    boolean = true

    Disable/enable lazy init of server app factory.

    Dev server options

    Following options are used to customize the companion dev server location (only for dev command). These can be useful if you have some special dev environment, where you have an issue with the default configuration.

    note

    If you provide port and hostname, you don't need to define the publicUrl, the CLI will create it automatically, unless the publicUrl is completely different than the hostname and port provided.

    --port

    number

    Dev server port.

    --hostname

    string

    Dev server hostname, for example: localhost, or 127.0.0.1.

    --publicUrl

    string

    Dev server public url, for example: http://localhost:3101.

    - +

    Introduction to @ima/cli

    The IMA.js CLI allows you to build and watch your application for changes during development. These features are handle by the only two currently supported commands build and dev.

    You can always list available commands by running:

    npx ima --help
    note

    npx comes pre-installed with npm 5.2+ and higher.

    This should produce following output:

    Usage: ima <command>

    Commands:
    ima build Build an application for production
    ima dev Run application in development watch mode

    Options:
    --version Show version number [boolean]
    --help Show help [boolean]

    Development

    The npx ima dev command starts the application in the development mode with HMR, error-overlay, source maps and other debugging tools enabled.

    By default the application starts on http://localhost:3001 with companion dev server running at http://localhost:3101. These can be further customized through the app environment settings and CLI arguments.

    You can also run npx ima dev --help to list all available options that you can use:

    ima dev

    Run application in development watch mode

    Options:
    --version Show version number [boolean]
    --help Show help [boolean]
    --clean Clean build folder before building the application [boolean] [default: true]
    --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]
    --verbose Use default webpack CLI output instead of custom one [boolean]
    --inspect Enable Node inspector mode [boolean]
    --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]
    --open Opens browser window after server has been started [boolean] [default: true]
    --openUrl Custom URL used when opening browser window [string]
    --legacy Runs application in legacy mode [boolean] [default: false]
    --forceLegacy Forces runner.js to execute legacy client code [boolean] [default: false]
    --forceSPA Forces application to run in SPA mode [boolean] [default: false]
    --writeToDisk Write static files to disk, instead of serving it from memory [boolean] [default: false]
    --reactRefresh Enable/disable react fast refresh for React components [boolean] [default: true]
    --lazyServer Enable/disable lazy init of server app factory [boolean] [default: true]
    --port Dev server port (overrides ima.config.js settings) [number]
    --hostname Dev server hostname (overrides ima.config.js settings) [string]
    --publicUrl Dev server publicUrl (overrides ima.config.js settings) [string]
    info

    Any of the above mentioned options can be combined together in all different combinations and all options have specified default value. This means that in normal cases you can run npx ima dev without any additional arguments.

    Build

    Builds the application in production mode with all optimizations enabled (compression, minification, etc.). The build command drops some options compared to the dev command. While adding few build specific commands. npx build --help produces:

    ima build

    Build an application for production

    Options:
    --version Show version number [boolean]
    --help Show help [boolean]
    --clean Clean build folder before building the application [boolean] [default: true]
    --clearCache Deletes node_modules/.cache directory to invalidate loaders cache [boolean]
    --verbose Use default webpack CLI output instead of custom one [boolean]
    --ignoreWarnings Webpack will no longer print warnings during compilation [boolean]
    --profile Turn on profiling support in production [boolean] [default: false]

    CLI options

    Most of the following options are available for both dev and build commands, however some may be exclusive to only one of them. You can always use the --help argument to show all available options for each command.

    tip

    When you run into any issues with the application build, you can always run the app with npx ima dev --clearCache to make sure that all cache and tmp files are deleted before next build and see if this resolves your issues.

    Similarly you can use the --verbose option to show more information during build that can aid you in debugging process in case anything happens.

    --version

    Prints @ima/cli version.

    --help

    Prints help dialog.

    --clean

    boolean = true

    Deletes ./build folder before running the application.

    --clearCache

    boolean = false

    Clears ./node_modules/.cache folder. This is used to store webpack filesystem cache and other webpack loader and plugins cache.

    --verbose

    boolean = false

    Disables custom CLI logging style in favor of default webpack CLI verbose. This can be useful for debugging.

    --inspect

    boolean = false

    Disable/enable node inspector mode.

    --ignoreWarnings

    boolean = false

    Ignore reporting of webpack warning messages. The CLI automatically caches all existing warnings and shows just new warnings rebuilds in watch mode.

    --open

    boolean = true

    Enable/disable auto opening of app URL in the browser window on startup.

    tip

    If you find this option annoying, you can completely disable this feature across all IMA.js applications by putting IMA_CLI_OPEN=false in your environment.

    --openUrl

    boolean = true

    Allows you to customize URL which is opened when the server starts in development mode.

    tip

    You can also use IMA_CLI_OPEN_URL='http://ima.dev:3001' env variable to set this option.

    This is usefull when you have project-specific URLs. You can then set this environment variable in application's ima.config.js and don't have to worry about using --openUrl CLI argument everytime you're starting the application in dev mode.

    --legacy

    boolean = false

    By default the CLI only builds es version of JS files in development mode. Use this option to enable additional build of non es version.

    --forceLegacy

    boolean = false

    Enables legacy mode and forces runner.js to load legacy code even if targeted browser supports the latest client es version.

    --forceSPA

    boolean = false

    Forces the application to run in SPA mode.

    --profile

    boolean = false

    Disables some optimizations to allow for better debugging while also trying to be as close to the production build as possible. Currently this option disables mangling of classes and functions, which produces more readable stack traces.

    --writeToDisk

    boolean = false

    By default the app client static files are served from memory in dev mode. Using this option you can force webpack to write these files and serve them from the disk.

    tip

    This option can be useful in some cases where you need to take a look at the compile source code, where it's easier to browse these files locally, rather than on the static server.

    --reactRefresh

    boolean = true

    Disable/enable react fast refresh for React components.

    tip

    Disable this option if you are watching and editing node_modules files, this may result in less performant but more stable HMR experience.

    --lazyServer

    boolean = true

    Disable/enable lazy init of server app factory.

    Dev server options

    Following options are used to customize the companion dev server location (only for dev command). These can be useful if you have some special dev environment, where you have an issue with the default configuration.

    note

    If you provide port and hostname, you don't need to define the publicUrl, the CLI will create it automatically, unless the publicUrl is completely different than the hostname and port provided.

    --port

    number

    Dev server port.

    --hostname

    string

    Dev server hostname, for example: localhost, or 127.0.0.1.

    --publicUrl

    string

    Dev server public url, for example: http://localhost:3101.

    + \ No newline at end of file diff --git a/cli/plugins/analyze-plugin/index.html b/cli/plugins/analyze-plugin/index.html index 509114a6f..593f83590 100644 --- a/cli/plugins/analyze-plugin/index.html +++ b/cli/plugins/analyze-plugin/index.html @@ -4,13 +4,13 @@ Analyze Plugin | IMA.js - +
    -

    Analyze Plugin

    Pre-configures bundle-stats-webpack-plugin and webpack-bundle-analyzer webpack plugins for fast and easy bundle analyzing.

    This plugin provides easy way to analyze webpack bundle. Apart from pre-configuring the forementioned plugins, it also outputs stats.json file which can be used in multiple other online webpack bundle analyzer tools. For example:

    note

    The plugin also prints these links directly into the console when the build finishes, for easier access.

    Installation

    npm install @ima/cli-plugin-analyze -D

    Usage

    ./ima.config.js
    const { AnalyzePlugin } = require('@ima/cli-plugin-analyze');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    plugins: [new AnalyzePlugin()],
    };

    CLI Arguments

    --analyze

    client | client.es | server

    Run the ima build command with --analyze argument and pick one of the three produced bundles you want to analyze. For example: npx ima build --analyze=client.

    Options

    new AnalyzePlugin(options: {
    open?: boolean;
    bundleStatsOptions?: BundleStatsWebpackPlugin.Options;
    bundleAnalyzerOptions?: BundleAnalyzerPlugin.Options;
    });

    open

    boolean = true

    Set to false if you don't want to automatically open the browser window with the html reports when the build finishes.

    bundleStatsOptions

    object

    Pass any option that the BundleStatsWebpackPlugin accepts. These are then merged with some of our custom defaults.

    bundleAnalyzerOptions

    object

    Pass any option that the BundleAnalyzerPlugin accepts. These are then merged with some of our custom defaults.

    - +

    Analyze Plugin

    Pre-configures bundle-stats-webpack-plugin and webpack-bundle-analyzer webpack plugins for fast and easy bundle analyzing.

    This plugin provides easy way to analyze webpack bundle. Apart from pre-configuring the forementioned plugins, it also outputs stats.json file which can be used in multiple other online webpack bundle analyzer tools. For example:

    note

    The plugin also prints these links directly into the console when the build finishes, for easier access.

    Installation

    npm install @ima/cli-plugin-analyze -D

    Usage

    ./ima.config.js
    const { AnalyzePlugin } = require('@ima/cli-plugin-analyze');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    plugins: [new AnalyzePlugin()],
    };

    CLI Arguments

    --analyze

    client | client.es | server

    Run the ima build command with --analyze argument and pick one of the three produced bundles you want to analyze. For example: npx ima build --analyze=client.

    Options

    new AnalyzePlugin(options: {
    open?: boolean;
    bundleStatsOptions?: BundleStatsWebpackPlugin.Options;
    bundleAnalyzerOptions?: BundleAnalyzerPlugin.Options;
    });

    open

    boolean = true

    Set to false if you don't want to automatically open the browser window with the html reports when the build finishes.

    bundleStatsOptions

    object

    Pass any option that the BundleStatsWebpackPlugin accepts. These are then merged with some of our custom defaults.

    bundleAnalyzerOptions

    object

    Pass any option that the BundleAnalyzerPlugin accepts. These are then merged with some of our custom defaults.

    + \ No newline at end of file diff --git a/cli/plugins/less-constants-plugin/index.html b/cli/plugins/less-constants-plugin/index.html index 6fe895b8e..775756f80 100644 --- a/cli/plugins/less-constants-plugin/index.html +++ b/cli/plugins/less-constants-plugin/index.html @@ -4,13 +4,13 @@ LESS Constants Plugin | IMA.js - +
    -

    LESS Constants Plugin

    Adds preprocessor which converts theme values defined in the JS file, to their LESS variable counterparts.

    Can be used to share theme variables between JS and LESS files or even multiple npm packages to allow for easier overrides.

    Installation

    npm install @ima/cli-plugin-less-constants -D

    Usage

    First create new plugin instance in the ima.config.js file:

    ./ima.config.js
    const { LessConstantsPlugin } = require('@ima/cli-plugin-less-constants');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    plugins: [
    new LessConstantsPlugin({
    entry: './app/config/theme.js'
    })
    ],
    };

    Create theme.js file with constants definitions

    Then export your LESS JS constants from the provided entry file, using the available units helper functions, imported from the CLI plugin:

    ./app/config/theme.js
    import { units, media } from '@ima/cli-plugin-less-constants/units';

    export default {
    bodyfontSize: units.rem(1),
    headerHeight: units.px(120),
    bodyWidth: units.vw(100),
    greaterThanMobile: media.maxWidthMedia(360, 'screen'),
    zIndexes: units.lessMap({
    header: 100,
    footer: 200,
    body: 1,
    }),
    };

    This produces the following output:

    ./build/less-constants/constants.less
    @bodyfont-size: 1rem;
    @header-height: 120px;
    @body-width: 100vw;
    @greater-than-mobile: ~"screen and (max-width: 360)";
    @z-indexes: {
    header: 100;
    footer: 200;
    body: 1;
    }

    Import generated constants.less in globals

    Finally don't forget to import the generated ./build/less-constants/constants.less file in your ./app/less/globals.less to have the variables available in all LESS files automatically without explicit import.

    ./app/less/globals.less
    @import "../../build/less-constants/constants.less";

    Usage in JavaScript

    Since every unit returns Unit object, you can always access it's value through the .valueOf() method or use the CSS interpreted value by calling .toString().

    import { headerHeight } from 'app/config/theme.js';

    export default function ThemeComponent({ children, title, href }) {
    return (
    <div>
    Header height has an absolute value of: {headerHeight.valueOf()} {/* 120 */},
    while it's CSS value is: {headerHeight.toString()} {/* 120px */}
    </div>
    );
    }
    caution

    The constants are generated only in the preProcess which runs just ones before the compilation. So make sure to restart the built manually, when you add any new constants, to allow for the regeneration of the constants.less file.

    Options

    new LessConstantsPlugin(options: {
    entry: string;
    output?: string;
    });

    entry

    string

    Path to the LESS constants JS file.

    output

    string

    Optional custom output path, defaults to ./build/less-constants/constants.less.

    Units

    The plugin provides unit functions for almost every unit available + some other helpers. Each helper returns Unit object with following interface:

    export interface Unit {
    valueOf: () => string;
    toString: () => string;
    }
    • Numeric values - em, ex, ch, rem, lh, rlh, vw, vh, vmin, vmax, vb, vi, svw, svh, lvw, lvh, dvw, dvh, cm, mm, Q, inches, pc, pt, px, percent.
    • Color values - hex, rgb, rgba, hsl, hsla.
    • Media queries - maxWidthMedia, minWidthMedia, minAndMaxWidthMedia, maxHeightMedia, minHeightMedia.
    • LESS map helper - lessMap can be used to group together similar values in an "object-like" value.

    Custom units

    If you're missing any additional helpers, you can always define your own, either custom ones (as long as they adhere to the Unit interface) or you can use the following helper:

    import { asUnit } from '@ima/cli-plugin-less-constants/units';

    function asUnit(
    unit: string,
    parts: (string | number)[],
    template = '${parts}${unit}'
    ): Unit {
    return {
    __propertyDeclaration: true,

    valueOf(): string {
    return parts.length === 1 ? parts[0].toString() : this.toString();
    },

    toString(): string {
    return template
    .replace('${parts}', parts.join(','))
    .replace('${unit}', unit);
    },
    };
    }
    - +

    LESS Constants Plugin

    Adds preprocessor which converts theme values defined in the JS file, to their LESS variable counterparts.

    Can be used to share theme variables between JS and LESS files or even multiple npm packages to allow for easier overrides.

    Installation

    npm install @ima/cli-plugin-less-constants -D

    Usage

    First create new plugin instance in the ima.config.js file:

    ./ima.config.js
    const { LessConstantsPlugin } = require('@ima/cli-plugin-less-constants');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    plugins: [
    new LessConstantsPlugin({
    entry: './app/config/theme.js'
    })
    ],
    };

    Create theme.js file with constants definitions

    Then export your LESS JS constants from the provided entry file, using the available units helper functions, imported from the CLI plugin:

    ./app/config/theme.js
    import { units, media } from '@ima/cli-plugin-less-constants/units';

    export default {
    bodyfontSize: units.rem(1),
    headerHeight: units.px(120),
    bodyWidth: units.vw(100),
    greaterThanMobile: media.maxWidthMedia(360, 'screen'),
    zIndexes: units.lessMap({
    header: 100,
    footer: 200,
    body: 1,
    }),
    };

    This produces the following output:

    ./build/less-constants/constants.less
    @bodyfont-size: 1rem;
    @header-height: 120px;
    @body-width: 100vw;
    @greater-than-mobile: ~"screen and (max-width: 360)";
    @z-indexes: {
    header: 100;
    footer: 200;
    body: 1;
    }

    Import generated constants.less in globals

    Finally don't forget to import the generated ./build/less-constants/constants.less file in your ./app/less/globals.less to have the variables available in all LESS files automatically without explicit import.

    ./app/less/globals.less
    @import "../../build/less-constants/constants.less";

    Usage in JavaScript

    Since every unit returns Unit object, you can always access it's value through the .valueOf() method or use the CSS interpreted value by calling .toString().

    import { headerHeight } from 'app/config/theme.js';

    export default function ThemeComponent({ children, title, href }) {
    return (
    <div>
    Header height has an absolute value of: {headerHeight.valueOf()} {/* 120 */},
    while it's CSS value is: {headerHeight.toString()} {/* 120px */}
    </div>
    );
    }
    caution

    The constants are generated only in the preProcess which runs just ones before the compilation. So make sure to restart the built manually, when you add any new constants, to allow for the regeneration of the constants.less file.

    Options

    new LessConstantsPlugin(options: {
    entry: string;
    output?: string;
    });

    entry

    string

    Path to the LESS constants JS file.

    output

    string

    Optional custom output path, defaults to ./build/less-constants/constants.less.

    Units

    The plugin provides unit functions for almost every unit available + some other helpers. Each helper returns Unit object with following interface:

    export interface Unit {
    valueOf: () => string;
    toString: () => string;
    }
    • Numeric values - em, ex, ch, rem, lh, rlh, vw, vh, vmin, vmax, vb, vi, svw, svh, lvw, lvh, dvw, dvh, cm, mm, Q, inches, pc, pt, px, percent.
    • Color values - hex, rgb, rgba, hsl, hsla.
    • Media queries - maxWidthMedia, minWidthMedia, minAndMaxWidthMedia, maxHeightMedia, minHeightMedia.
    • LESS map helper - lessMap can be used to group together similar values in an "object-like" value.

    Custom units

    If you're missing any additional helpers, you can always define your own, either custom ones (as long as they adhere to the Unit interface) or you can use the following helper:

    import { asUnit } from '@ima/cli-plugin-less-constants/units';

    function asUnit(
    unit: string,
    parts: (string | number)[],
    template = '${parts}${unit}'
    ): Unit {
    return {
    __propertyDeclaration: true,

    valueOf(): string {
    return parts.length === 1 ? parts[0].toString() : this.toString();
    },

    toString(): string {
    return template
    .replace('${parts}', parts.join(','))
    .replace('${unit}', unit);
    },
    };
    }
    + \ No newline at end of file diff --git a/cli/plugins/scramble-css-plugin/index.html b/cli/plugins/scramble-css-plugin/index.html index a6e683ef8..1d6028e77 100644 --- a/cli/plugins/scramble-css-plugin/index.html +++ b/cli/plugins/scramble-css-plugin/index.html @@ -4,13 +4,13 @@ ScrambleCSS Plugin | IMA.js - +
    -

    ScrambleCSS Plugin

    Implements CSS class minimizer and uglifier that can be reverse-compiled at runtime (you can access classes using their original name).

    It works by processing all CSS files using custom PostCSS plugin, that mangles (scrambles) and minimizes all classes, while also building translation table (hashtable.json) along the way.

    The result is CSS file with mangled class names and companion hashtable that we use in our custom $CssClasses processor to, translate existing classes used out components to the new scrambled ones.

    Requirements

    caution

    As mentioned above, for this feature to work you need to wrap all your classNames in cssClasses function. Otherwise you'll end up with scrambled classes in CSS file but original class names in your components.

    import { useComponent } from '@ima/react-hooks';

    export default function Card() {
    const { cssClasses } = useComponent();

    return (
    <div className={cssClasses('card')} />
    );
    }

    or in case of class components:

    import { AbstractPureComponent } from '@ima/react-page-renderer';

    export default class Card extends AbstractPureComponent {
    render() {
    return (
    <div className={this.cssClasses('card')} />
    );
    }
    }

    Installation

    npm install @ima/cli-plugin-scramble-css -D

    Usage

    ./ima.config.js
    const { ScrambleCssPlugin } = require('@ima/cli-plugin-scramble-css');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    plugins: [new ScrambleCssPlugin()],
    };

    $CssClasses override and hashtable.json

    We have to provide our custom $CssClasses processor and pass it our generate hashtable.json file. To do that, we're going to load it's contents in the app environment:

    ./server/config/environment.js
    const fs = require('fs');
    const path = require('path');

    const hashTablePath = path.resolve(
    __dirname,
    '../../build/static/css/hashTable.json'
    );

    module.exports = (() => {
    return {
    prod: {
    $App: {
    scrambleCss: {
    hashTable: fs.existsSync(hashTablePath)
    ? JSON.parse(fs.readFileSync(hashTablePath))
    : null,
    },
    },
    // ...
    }
    }
    });

    Finally, the hashtable is now available under config.$App.scrambleCss.hashTable, so we're going to provide it to the plugin's custom $CssClasses processor in the app bind.js file, and we're done:

    ./app/config/bind.js
    import { scrambleCssClasses } from '@ima/cli-plugin-scramble-css/scrambleCssClasses';

    export default (ns, oc, config) => {
    oc.bind(
    '$CssClasses',
    scrambleCssClasses(config?.$App?.scrambleCss?.hashTable),
    []
    );
    };

    CLI Arguments

    --scrambleCss

    boolean

    The scrambling is enabled by default for the production environment. However you can explicitly enable/disable it using this CLI argument. This applies for both CLI commands.

    Options

    new ScrambleCssPlugin(options: {
    scrambleCssMinimizerOptions?: {
    hashTableFilename?: string;
    mainAssetFilter?: (filename: string) => boolean;
    };
    });

    scrambleCssMinimizerOptions

    object

    These are passed directly into the ScrambleCssMinimizer. You can define custom:

    • hashTableFilename - custom translation hashtable.json filename. Defaults to: ./build/static/css/hashTable.json.
    • mainAssetFilter - should resolve to the main css file. The minimizer first processes the main.css file and generates the hashtable.json translation table. If you then want to process other assets with existing hashtable, these should be filtered out in this function, since the minimizer minimizes them in second pass using existing hashtable.json.
    note

    You should be fine with the default options in almost any situation except some special use cases.

    - +

    ScrambleCSS Plugin

    Implements CSS class minimizer and uglifier that can be reverse-compiled at runtime (you can access classes using their original name).

    It works by processing all CSS files using custom PostCSS plugin, that mangles (scrambles) and minimizes all classes, while also building translation table (hashtable.json) along the way.

    The result is CSS file with mangled class names and companion hashtable that we use in our custom $CssClasses processor to, translate existing classes used out components to the new scrambled ones.

    Requirements

    caution

    As mentioned above, for this feature to work you need to wrap all your classNames in cssClasses function. Otherwise you'll end up with scrambled classes in CSS file but original class names in your components.

    import { useComponent } from '@ima/react-hooks';

    export default function Card() {
    const { cssClasses } = useComponent();

    return (
    <div className={cssClasses('card')} />
    );
    }

    or in case of class components:

    import { AbstractPureComponent } from '@ima/react-page-renderer';

    export default class Card extends AbstractPureComponent {
    render() {
    return (
    <div className={this.cssClasses('card')} />
    );
    }
    }

    Installation

    npm install @ima/cli-plugin-scramble-css -D

    Usage

    ./ima.config.js
    const { ScrambleCssPlugin } = require('@ima/cli-plugin-scramble-css');

    /**
    * @type import('@ima/cli').ImaConfig
    */
    module.exports = {
    plugins: [new ScrambleCssPlugin()],
    };

    $CssClasses override and hashtable.json

    We have to provide our custom $CssClasses processor and pass it our generate hashtable.json file. To do that, we're going to load it's contents in the app environment:

    ./server/config/environment.js
    const fs = require('fs');
    const path = require('path');

    const hashTablePath = path.resolve(
    __dirname,
    '../../build/static/css/hashTable.json'
    );

    module.exports = (() => {
    return {
    prod: {
    $App: {
    scrambleCss: {
    hashTable: fs.existsSync(hashTablePath)
    ? JSON.parse(fs.readFileSync(hashTablePath))
    : null,
    },
    },
    // ...
    }
    }
    });

    Finally, the hashtable is now available under config.$App.scrambleCss.hashTable, so we're going to provide it to the plugin's custom $CssClasses processor in the app bind.js file, and we're done:

    ./app/config/bind.js
    import { scrambleCssClasses } from '@ima/cli-plugin-scramble-css/scrambleCssClasses';

    export default (ns, oc, config) => {
    oc.bind(
    '$CssClasses',
    scrambleCssClasses(config?.$App?.scrambleCss?.hashTable),
    []
    );
    };

    CLI Arguments

    --scrambleCss

    boolean

    The scrambling is enabled by default for the production environment. However you can explicitly enable/disable it using this CLI argument. This applies for both CLI commands.

    Options

    new ScrambleCssPlugin(options: {
    scrambleCssMinimizerOptions?: {
    hashTableFilename?: string;
    mainAssetFilter?: (filename: string) => boolean;
    };
    });

    scrambleCssMinimizerOptions

    object

    These are passed directly into the ScrambleCssMinimizer. You can define custom:

    • hashTableFilename - custom translation hashtable.json filename. Defaults to: ./build/static/css/hashTable.json.
    • mainAssetFilter - should resolve to the main css file. The minimizer first processes the main.css file and generates the hashtable.json translation table. If you then want to process other assets with existing hashtable, these should be filtered out in this function, since the minimizer minimizes them in second pass using existing hashtable.json.
    note

    You should be fine with the default options in almost any situation except some special use cases.

    + \ No newline at end of file diff --git a/contributing/how-to-contribute/index.html b/contributing/how-to-contribute/index.html index 6ad5360ab..a68cd55d8 100644 --- a/contributing/how-to-contribute/index.html +++ b/contributing/how-to-contribute/index.html @@ -4,13 +4,13 @@ How to Contribute | IMA.js - +
    -

    How to Contribute

    Contribute to this project via Pull Requests.

    Changesets

    We are using changesets for the release management. Adding a changeset file to a Pull Request is required in most cases as it triggers the release of the affected packages. If your changes don't affect any package (documentation/tests update, or change in the repository workflow), then you can skip adding a changeset file.

    Read on how to add a changeset in the official changesets documentation. In our repositories, you can use npm run changeset alias to open the changeset prompt.

    Semantic Versioning

    IMA.js follows semantic versioning. We release patch versions for bugfixes, minor versions for new features, and major versions for any breaking changes.

    Every significant change is documented in the changelog file of the related package.

    Open Development

    All work on IMA.js happens directly on GitHub. Both core team members and external contributors send pull requests which go through the same review process.

    Branch Organization

    There are 2 main branches, master and next.

    Branch master contains the current stable version. You should target your Pull Request here, if your changes are adding a new feature, fixing a bug, or any other change that does not require a major bump. New version from this branch will be published to the official npm registry under the latest tag.

    Branch next contains the next major release candidate version. You should target your Pull Request here, if you are introducing a breaking change, or extending a functionality existing only in this branch. New version from this branch will be published to the official npm registry under the rc tag.

    Development Workflow

    After cloning IMA.js repository, run npm ci (check .nvmrc file for supported Node.js version) to fetch its dependencies. Then, you can run several commands:

    • npm run lint checks the code style.
    • npm run lint -- --fix fixes the code style issues.
    • npm run stylelint checks the css/less code style.
    • npm run stylelint -- --fix fixes the css/less code style issues.
    • npm run test runs only tests affected by your changes.
    • npm run test -- --watch runs an interactive test watcher.
    • npm run test <pattern> runs tests with matching filenames.
    • npm run test:all runs the complete test suite.
    • npm run test:size runs size check to avoid introduction of large bundles.
    • npm run build creates a build folder within all the packages.
    • npm run changeset opens the changesets prompt.

    We recommend running npm run test (or its variations above) to make sure you don’t introduce any regressions as you work on your change.

    License

    By contributing to IMA.js, you agree that your contributions will be licensed under its MIT license.

    - +

    How to Contribute

    Contribute to this project via Pull Requests.

    Changesets

    We are using changesets for the release management. Adding a changeset file to a Pull Request is required in most cases as it triggers the release of the affected packages. If your changes don't affect any package (documentation/tests update, or change in the repository workflow), then you can skip adding a changeset file.

    Read on how to add a changeset in the official changesets documentation. In our repositories, you can use npm run changeset alias to open the changeset prompt.

    Semantic Versioning

    IMA.js follows semantic versioning. We release patch versions for bugfixes, minor versions for new features, and major versions for any breaking changes.

    Every significant change is documented in the changelog file of the related package.

    Open Development

    All work on IMA.js happens directly on GitHub. Both core team members and external contributors send pull requests which go through the same review process.

    Branch Organization

    There are 2 main branches, master and next.

    Branch master contains the current stable version. You should target your Pull Request here, if your changes are adding a new feature, fixing a bug, or any other change that does not require a major bump. New version from this branch will be published to the official npm registry under the latest tag.

    Branch next contains the next major release candidate version. You should target your Pull Request here, if you are introducing a breaking change, or extending a functionality existing only in this branch. New version from this branch will be published to the official npm registry under the rc tag.

    Development Workflow

    After cloning IMA.js repository, run npm ci (check .nvmrc file for supported Node.js version) to fetch its dependencies. Then, you can run several commands:

    • npm run lint checks the code style.
    • npm run lint -- --fix fixes the code style issues.
    • npm run stylelint checks the css/less code style.
    • npm run stylelint -- --fix fixes the css/less code style issues.
    • npm run test runs only tests affected by your changes.
    • npm run test -- --watch runs an interactive test watcher.
    • npm run test <pattern> runs tests with matching filenames.
    • npm run test:all runs the complete test suite.
    • npm run test:size runs size check to avoid introduction of large bundles.
    • npm run build creates a build folder within all the packages.
    • npm run changeset opens the changesets prompt.

    We recommend running npm run test (or its variations above) to make sure you don’t introduce any regressions as you work on your change.

    License

    By contributing to IMA.js, you agree that your contributions will be licensed under its MIT license.

    + \ No newline at end of file diff --git a/devtools/devtools-introduction/index.html b/devtools/devtools-introduction/index.html index 8212366a7..c2342a49b 100644 --- a/devtools/devtools-introduction/index.html +++ b/devtools/devtools-introduction/index.html @@ -4,7 +4,7 @@ Introduction | IMA.js - + @@ -27,8 +27,8 @@ wrap IMA.js app method calls in Proxy-like objects, which then before execution, send an information about it's call, arguments and payload to the devtools panel. The panel then batch-processes these messages and displays them.

    It can be customized through extension's options, where you can define what exactly should be wrapped using proxies and how it should be pre-processed before sending it to the user. In the -next section we're going to talk about the devtools UI and it's components.

    - +next section we're going to talk about the devtools UI and it's components.

    + \ No newline at end of file diff --git a/devtools/devtools-options/index.html b/devtools/devtools-options/index.html index d3af17830..712f397d7 100644 --- a/devtools/devtools-options/index.html +++ b/devtools/devtools-options/index.html @@ -4,7 +4,7 @@ Options | IMA.js - + @@ -35,8 +35,8 @@ in case of npm package.
  • module {?string} - used for named exports, can be left blank in case of default exports.
  • Message colors

    Currently there are 13 colors (we're using the Open Color color scheme), that you can use to differentiate each hook with:

    NameColor

    Conclusion

    You should by ok with the default preset for most cases but in case you head into defining a custom one, here are some notes on this matter.

    Defining custom hooks requires some more knowledge into the devtools that you can get by studying the devtool script to see how things work. We suggest to start by customizing the default set, changing few rules or splitting -existing hooks into more smaller ones, before heading in and defining whole new preset.

    - +existing hooks into more smaller ones, before heading in and defining whole new preset.

    + \ No newline at end of file diff --git a/devtools/devtools-ui/index.html b/devtools/devtools-ui/index.html index 48d2e8128..1f66ab814 100644 --- a/devtools/devtools-ui/index.html +++ b/devtools/devtools-ui/index.html @@ -4,7 +4,7 @@ UI & Controls | IMA.js - + @@ -47,8 +47,8 @@ you're currently visiting is using IMA.js and devtools are initialized. As a bonus you can also see the application environment, language and version.

    Options

    This is a separate page, where you can configure the script that is injected into the page and customize it to your needs. As this is rather more complicated we're going to take -a deeper look into it in the next section.

    - +a deeper look into it in the next section.

    + \ No newline at end of file diff --git a/index.html b/index.html index 1f2d94ef5..864c7a159 100644 --- a/index.html +++ b/index.html @@ -4,13 +4,13 @@ IMA.js | A Javascript framework for creating isomorphic applications | IMA.js - +

    IMA.js

    A Javascript framework for creating isomorphic applications.

    Features

    Fully Isomorphic

    Write and run the same code at both the server side and the client side! IMA.js provides abstraction for APIs that differ at the client side javascript and the server side javascript.

    SEO

    Page metadata are centrally managed, allowing easier management of all your keywords and og meta-tags.

    Benchmarks and Tests

    Real-world heavy-load web services are run on the IMA.js platform. With hundreds of unit tests covering all of our code, you can rely on IMA.js to be a stable base of your application.

    Production-ready Full Application Stack

    Use the familiar MVC pattern in combination with React for rendering your UI. See Hello example.

    Routing

    IMA.js comes with a built-in router for processing GET and POST HTTP requests.

    Bleeding Edge Technologies

    ESNext, Webpack, SWC compiler, HMR and other technologies.

    Full plugin support

    IMA.js comes with full support for plugins that can be created very easily through very simple interface.

    Large collection of existing plugins

    We maintain a list of several plugins that are thoroughly tested and used every day on many of our websites here at Seznam.cz along with huge list of other plugins we use internally.

    Multiple running modes

    Application can be switched between IMA, SPA and MPA modes or combination of all of them.

    And many more...

    Configuration for all your environments in one place with inheritance.
    Out-of-box configurable server-side caching.
    REST API cache.
    Advanced error handling for greater stability and faster development.
    High-fidelity debug mode.
    Dependency injection.
    Analytics plugin, that provides interface to custom analytics, along with FB pixel and Google Analytics plugins.
    Generic REST API client plugin for the IMA application framework
    Plugin to simplify loading 3rd party scripts for the IMA.js application
    Reselect-style page state selector plugin for IMA.js components.
    And many more…

    Who's using IMA.js

    Seznamzpravy.cz

    Seznamzpravy.cz

    Seznam Zprávy is one of the top Czech news platforms, which delivers compelling, diverse and visually engaging stories in the form of a text and a video.

    Novinky.cz

    Novinky.cz

    One of the biggest and most visited news website in the Czech Republic. It offers coverage of the latest news from home and around the world in one place.

    Prozeny.cz

    Prozeny.cz

    The biggest online lifestyle magazine for women in the Czech Republic. Current articles about fashion, health and lifestyle, living, family, and a great section of recipes.

    Garaz.cz

    Garaz.cz

    The biggest online magazine about cars and motorcycles in the Czech Republic where you can found current articles about news from the car industry, service tips, reports from car events and wonderful video section.

    Počasí.cz

    Počasí.cz

    The most visited weather forecast website in the Czech republic. Offers weather forecast up to 6 days ahead for every place on Earth and interactive map with meteoradar.

    tv.seznam.cz

    tv.seznam.cz

    TV broadcast schedule for at least 14 days ahead, available for more than 100 most watched TV stations. Horizontal and vertical layout, notifications and social sharing is available.

    SBazar.cz

    SBazar.cz

    Sbazar.cz is one of the largest online peer to peer marketers in the Czech Republic. People can choose between 1,800,000 current offers. Advertising is clearly arranged into individual categories with the possibility of searching in a specific place in the Czechia.

    SAuto.cz

    SAuto.cz

    Sauto is the biggest marketplace of used and new cars in the Czech Republic. People can choose from up to 90,000 current sellers’ ads from across the country. Advanced filtering makes finding a car easier.

    Hry.cz

    Hry.cz

    The Czech video game portal where you find your next video game to play. There are plenty of video games for different devices, including on-line browser games.

    Seznam.cz/vychytavky

    Seznam.cz/vychytavky

    The news feed of the Seznam.cz company where you can see the new features of all Seznam.cz services.

    - + \ No newline at end of file diff --git a/introduction/configuration/index.html b/introduction/configuration/index.html index 8602eaade..17c65f251 100644 --- a/introduction/configuration/index.html +++ b/introduction/configuration/index.html @@ -4,7 +4,7 @@ Configuration options | IMA.js - + @@ -33,8 +33,8 @@ prefixed by dollar sign $. Note that, again, the dev and test environment configuration automatically inherits values from the prod environment.

  • and finally, the app/config/bind.js configures the -Object container.

  • All of these files are necessary and must remain in their locations.

    - +Object container.

    All of these files are necessary and must remain in their locations.

    + \ No newline at end of file diff --git a/introduction/getting-started/index.html b/introduction/getting-started/index.html index a71753cbb..d6cfcfa15 100644 --- a/introduction/getting-started/index.html +++ b/introduction/getting-started/index.html @@ -4,7 +4,7 @@ Getting Started with IMA.js | IMA.js - + @@ -30,8 +30,8 @@ similar to the dev environment. To install the IMA.js application, start by cloning your application git repository on your production server:

    git clone [your-application-git-repository]

    Switch to the cloned directory and run the following commands to set-up your application - same as in the development mode - and build it:

    npm install
    npm run build

    Now after building your IMA.js application your server is ready to run it. You can start your application using the following command:

    npm run start

    Your application is now running at http://localhost:3001/ -(unless configured otherwise).

    - +(unless configured otherwise).

    + \ No newline at end of file diff --git a/migration/migration-0.14.0/index.html b/migration/migration-0.14.0/index.html index 67b6edc7c..e1d2fd548 100644 --- a/migration/migration-0.14.0/index.html +++ b/migration/migration-0.14.0/index.html @@ -4,13 +4,13 @@ Migration 0.14.0 | IMA.js - +
    -

    Migration 0.14.0

    In order to upgrade your project to use IMA.js 0.14.0, please follow these steps:

    • Update your gulpfile.js to require the default configuration from the ima-gulp-tasks package in the gulpfile.js (see the example configuration).
    • If you are using custom build and dev tasks, remove the Es6toEs5:ima task from those.
    • Update your own custom gulp tasks to be compatible with gulp 4
    • Remove references to the ima.client.js file in the settings.js file and the bundle section in the build.js file.
    • Add the ima package to the common group in the vendors section in the build.js file.
    • Update your main.js file, the ima.onLoad method returns a promise instead of accepting a callback.
    • Update your ima-server installation according to the Hello World example.
    • Plugins can no longer use namespaces, please update your bind.js file if you were using namespace references to IMA plugins.
    • Import the RouteNames constants from ima/router/RouteNames in your router.js configuration file.
    • Components may now declare the defaultProps and propTypes static properties as getters.
    • The $ROUTER_CONSTANTS alias no longer exists (import the ima/router/RouteNames file).
    • The $HTTP_STATUS_CODE alias no longer exists (import the ima/http/StatusCode file).
    • Removed the $Promise, $CacheEntry, $PageRendererViewAdapter, $Route (switched to imports internally).
    • The loose mode of the ES2015 babel preset is no longer enabled.
    • Upgrade to node.js 7 or newer (older version might work but are no longer supported).
    • Switch to default exports in your configuration files.
    • Remove the './node_modules/ima-babel6-polyfill/index.js' reference from the polyfills list in gulpConfig.js (if overridden; this has been fixed by babel).
    • Remove the main.less file reference in the build.js file (unless it exists in the project).
    • Add the $CssClasses property to object in the $Utils OC alias.
    - +

    Migration 0.14.0

    In order to upgrade your project to use IMA.js 0.14.0, please follow these steps:

    • Update your gulpfile.js to require the default configuration from the ima-gulp-tasks package in the gulpfile.js (see the example configuration).
    • If you are using custom build and dev tasks, remove the Es6toEs5:ima task from those.
    • Update your own custom gulp tasks to be compatible with gulp 4
    • Remove references to the ima.client.js file in the settings.js file and the bundle section in the build.js file.
    • Add the ima package to the common group in the vendors section in the build.js file.
    • Update your main.js file, the ima.onLoad method returns a promise instead of accepting a callback.
    • Update your ima-server installation according to the Hello World example.
    • Plugins can no longer use namespaces, please update your bind.js file if you were using namespace references to IMA plugins.
    • Import the RouteNames constants from ima/router/RouteNames in your router.js configuration file.
    • Components may now declare the defaultProps and propTypes static properties as getters.
    • The $ROUTER_CONSTANTS alias no longer exists (import the ima/router/RouteNames file).
    • The $HTTP_STATUS_CODE alias no longer exists (import the ima/http/StatusCode file).
    • Removed the $Promise, $CacheEntry, $PageRendererViewAdapter, $Route (switched to imports internally).
    • The loose mode of the ES2015 babel preset is no longer enabled.
    • Upgrade to node.js 7 or newer (older version might work but are no longer supported).
    • Switch to default exports in your configuration files.
    • Remove the './node_modules/ima-babel6-polyfill/index.js' reference from the polyfills list in gulpConfig.js (if overridden; this has been fixed by babel).
    • Remove the main.less file reference in the build.js file (unless it exists in the project).
    • Add the $CssClasses property to object in the $Utils OC alias.
    + \ No newline at end of file diff --git a/migration/migration-0.15.0/index.html b/migration/migration-0.15.0/index.html index 2230f50ce..4a01c04a2 100644 --- a/migration/migration-0.15.0/index.html +++ b/migration/migration-0.15.0/index.html @@ -4,7 +4,7 @@ Migration 0.15.0 | IMA.js - + @@ -13,8 +13,8 @@ If you are't overriding polyfills or shims, you can skip this step.

    Example:

    shim: {
    js: {
    name: 'shim.js',
    src: ['./node_modules/ima/polyfill/collectionEnumeration.js'],
    dest: {
    client: './build/static/js/'
    }
    },
    es: {
    name: 'shim.es.js',
    src: [],
    dest: {
    client: './build/static/js/',
    server: './build/ima/'
    }
    }


    polyfill: {
    js: {
    name: 'polyfill.js',
    src: [
    './node_modules/babel-polyfill/dist/polyfill.min.js',
    './node_modules/custom-event-polyfill/custom-event-polyfill.js'
    ],
    dest: {
    client: './build/static/js/'
    }
    },
    es: {
    name: 'polyfill.es.js',
    src: ['./node_modules/custom-event-polyfill/custom-event-polyfill.js'],
    dest: {
    client: './build/static/js/'
    }
    },
    fetch: {
    name: 'fetch-polyfill.js',
    src: [
    './node_modules/core-js/client/shim.min.js',
    './node_modules/whatwg-fetch/fetch.js'
    ],
    dest: {
    client: './build/static/js/'
    }
    },
    ima: {
    name: 'ima-polyfill.js',
    src: [
    './node_modules/ima/polyfill/imaLoader.js',
    './node_modules/ima/polyfill/imaRunner.js'
    ],
    dest: {
    client: './build/static/js/'
    }
    }
    }

    In build.js add new property 'es' to bundle object:

    es: [
    './build/static/js/polyfill.es.js',
    './build/static/js/shim.es.js',
    './build/static/js/vendor.client.es.js',
    './build/static/js/app.client.es.js'
    ]

    Add to your settings.js prod.$Page.$Render new property esScripts like this:

    esScripts: [
    '/static/js/locale/' + config.$Language + '.js' + versionStamp,
    '/static/js/app.bundle.es.min.js' + versionStamp
    ]

    Add to your settings.js dev.$Page.$Render new property esScripts like this:

    esScripts: [
    '/static/js/polyfill.es.js' + versionStamp,
    '/static/js/shim.es.js' + versionStamp,
    '/static/js/vendor.client.es.js' + versionStamp,
    `/static/js/locale/${config.$Language}.js${versionStamp}`,
    '/static/js/app.client.es.js' + versionStamp,
    '/static/js/hot.reload.js' + versionStamp
    ]

    Karma removed instead of that added Jest

    If you are overriding gulpfile.js you need to make following changes:

    • remove from gulpConfig.tasks.dev task test:unit:karma:dev
    • remove from gulpConfig.tasks.dev and gulpConfig.tasks.build task Es6ToEs5:vendor:client:test
    • remove from function buildExample task Es6ToEs5:vendor:client:test

    If you are overriding gulpConfig.tasks.build in gulpConfig.js you need to add bundle:es:app into bundles section.

    Server

    In server.js

    Add at the top into import sections:

    require('ima/polyfill/imaLoader.js');
    require('ima/polyfill/imaRunner.js');

    add proxy into middlewares imports section

    let proxy = require('express-http-proxy');

    change line

    .use(environment.$Proxy.path + '/', proxy)

    to

    .use(environment.$Proxy.path + '/', proxy(environment.$Proxy.server))

    DocumentView

    In DocumentView.jsx we united sync and async scripts.

    • remove getSyncScripts function.
    • update getAsyncScripts function to
    getAsyncScripts() {
    let scriptResources = `<script>
    function checkAsyncAwait () {
    try {
    new Function('(async () => ({}))()');
    return true;
    } catch (e) {
    return false;
    }
    }
    $IMA.Runner = $IMA.Runner || {};
    if (Object.values && checkAsyncAwait()) {
    $IMA.Runner.scripts = [
    ${this.utils.$Settings.$Page.$Render.esScripts
    .map(script => `'${script}'`)
    .join()}
    ];
    } else {
    $IMA.Runner.scripts = [
    ${this.utils.$Settings.$Page.$Render.scripts
    .map(script => `'${script}'`)
    .join()}
    ];
    }

    if (!window.fetch) {
    $IMA.Runner.scripts.unshift('${this.utils.$Settings.$Static
    .js}/fetch-polyfill.js');
    }

    $IMA.Runner.scripts.forEach(function(source) {
    var script = document.createElement('script');
    script.async = $IMA.$Env !== 'dev';
    script.onload = $IMA.Runner.load;
    script.src = source;

    document.getElementById('scripts').appendChild(script);
    });
    </script>`;

    return scriptResources;
    }

    replace

    {this.utils.$Settings.$Env === 'dev' ?
    <div id='scripts'>{this.getSyncScripts()}</div>
    :
    <div id='scripts' dangerouslySetInnerHTML={
    { __html: this.getAsyncScripts() }
    }/>
    }

    with

    <div id='scripts' dangerouslySetInnerHTML={
    { __html: this.getAsyncScripts() }
    }/>

    SPA

    In app/assets/static/html/spa.html add ima-polyfill.

    Removed namespaces

    If you extends some view from ns.ima.page.AbstractComponent, you need to add this import:

    import AbstractComponent from 'ima/page/AbstractComponent';

    and use AbstractComponent instead of ns.ima.page.AbstractComponent.

    If you extends some view from ns.ima.controller.AbstractController, you need to add this import:

    import AbstractController from 'ima/controller/AbstractController';

    and use AbstractController instead of ns.ima.controller.AbstractController.

    In settings.js import your DocumentView like this:

    import DocumentView from 'app/component/document/DocumentView';

    Now you need to replace your documentView namespace with React component

    so for this step replace your configuration.prod.$Page.$Render.documentView to DocumentView.

    Others

    • IMA.js is now using React 16 where is no longer supported react-addons-perf package in case you were using it.
    • There was added a fetchOptions property to the IMA.js' http. You can add this property into your settings.js file to the $Http.defaultRequestOptions object. The property represents the second and optional parameter of the fetch method https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch - an options object containing -settings that you want to apply to the Fetch API https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API request.
    • There is a breaking change in the IMA.js' router. Now there is defined an order where mandatory parameters have to be before optional parameters.
    • There are no longer available Request and Response at the client side.
    - +settings that you want to apply to the Fetch API https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API request.
  • There is a breaking change in the IMA.js' router. Now there is defined an order where mandatory parameters have to be before optional parameters.
  • There are no longer available Request and Response at the client side.
  • + \ No newline at end of file diff --git a/migration/migration-0.16.0/index.html b/migration/migration-0.16.0/index.html index 4b087e8ee..a7c4528c0 100644 --- a/migration/migration-0.16.0/index.html +++ b/migration/migration-0.16.0/index.html @@ -4,13 +4,13 @@ Migration 0.16.0 | IMA.js - +
    -

    Migration 0.16.0

    Upgrading from version 15 should be pretty straightforward and your application should work with no to minimal changes required.

    Changes in dependencies

    • babel-eslint: 8.2.2    10.0.1
    • enzyme: 3.7.0    3.8.0
    • enzyme-adapter-react-16: 1.1.1    1.7.1
    • eslint: 4.19.1    5.11.1
    • eslint-config-prettier: 2.9.0    3.3.0
    • eslint-plugin-jasmine: 2.9.3    2.10.1
    • eslint-plugin-jest: 21.15.0    22.1.2
    • eslint-plugin-prettier: 2.6.0    3.0.1
    • eslint-plugin-react: 7.7.0    7.12.0
    • ima-gulp-tasks: 0.15.0    ^0.16.0
    • jest: 22.4.3    23.6.0
    • express-http-proxy: 1.1.0    1.5.0
    • helmet: 3.12.0    3.15.0
    • ima: 0.15.1    0.16.0
    • ima-server: 0.15.0    0.16.0
    • method-override: 2.3.10    3.0.0
    • multer: 1.3.0    1.4.1
    • react: 16.2.0    16.7.0
    • react-dom: 16.2.0    16.7.0
    • enzyme-to-json: ^3.3.5 (new)
    • jest-serializer-enzyme (removed)
    • react-test-renderer (removed)

    Migration guide

    • Use of regular expressions in serveSPA.blacklist. If you've been using array of strings, you need to change the syntax to use regular expressions instead:
    /* app/environment.js */
    {
    ...
    - blackList: ['Googlebot', 'SeznamBot'],
    + blackList: userAgent => (new RegExp('Googlebot|SeznamBot', 'g')).test(userAgent),
    ...
    }
    • Added new mandatory parameter action to route() method in Router of a type { type: string, event: Event|null, url: string|null }, where type attribute can take one of these values: const { REDIRECT, CLICK, POP_STATE, ERROR } = ActionTypes.
    /* ima/Router/ClientRouter.js */
    route(
    path,
    options = {},
    { event = null, type = ActionTypes.REDIRECT, url = null } = {}
    )
    • Http options withCredentials is set to false by default. You must check your HTTP CORS requests and you must set withCredentials to true for sending Cookie header.

    • New serializer settings in Jest, to configure it first add path to a newly created file jest.setup.js (located in the root directory) into setupFiles array in jest.config.json. After that add new snapshotSerializers field with ["enzyme-to-json/serializer"] value, that handles module loading, which converts Enzyme wrappers into format compatible with Jest snapshots.

    /* jest.config.json */
    "setupFiles": [
    "ima/test.js",
    "ima/polyfill/imaLoader.js",
    "ima/polyfill/imaRunner.js",
    + "<rootDir>/jest.setup.js"
    ],
    + snapshotSerializers": ["enzyme-to-json/serializer"]
    /* jest.setup.js */
    const enzyme = require('enzyme');
    const Adapter = require('enzyme-adapter-react-16');

    enzyme.configure({ adapter: new Adapter() });

    Other changes

    • IMA.js-helpers - removed throttle and debounce functions. You can replace throttle with throttle method available in UIComponentHelper in IMA.js-ui-atoms package. 73843d0
    • Updated to React 16.7, which along with new context API and other features introduced deprecation of componentWillMount, componentWillReceiveProps and componentWillUpdate lifecycle methods.
    • Upgraded to Babel 7 and latest ESLint 5, which may result on some changes specific to your application.

    New Features

    • Most notable new feature is the introduction of PageManagerHandlers which adds the possibility to customize actions like saving and restoring scroll positions, setting browser's address bar URL etc. For more information visit the documentation.
    - +

    Migration 0.16.0

    Upgrading from version 15 should be pretty straightforward and your application should work with no to minimal changes required.

    Changes in dependencies

    • babel-eslint: 8.2.2    10.0.1
    • enzyme: 3.7.0    3.8.0
    • enzyme-adapter-react-16: 1.1.1    1.7.1
    • eslint: 4.19.1    5.11.1
    • eslint-config-prettier: 2.9.0    3.3.0
    • eslint-plugin-jasmine: 2.9.3    2.10.1
    • eslint-plugin-jest: 21.15.0    22.1.2
    • eslint-plugin-prettier: 2.6.0    3.0.1
    • eslint-plugin-react: 7.7.0    7.12.0
    • ima-gulp-tasks: 0.15.0    ^0.16.0
    • jest: 22.4.3    23.6.0
    • express-http-proxy: 1.1.0    1.5.0
    • helmet: 3.12.0    3.15.0
    • ima: 0.15.1    0.16.0
    • ima-server: 0.15.0    0.16.0
    • method-override: 2.3.10    3.0.0
    • multer: 1.3.0    1.4.1
    • react: 16.2.0    16.7.0
    • react-dom: 16.2.0    16.7.0
    • enzyme-to-json: ^3.3.5 (new)
    • jest-serializer-enzyme (removed)
    • react-test-renderer (removed)

    Migration guide

    • Use of regular expressions in serveSPA.blacklist. If you've been using array of strings, you need to change the syntax to use regular expressions instead:
    /* app/environment.js */
    {
    ...
    - blackList: ['Googlebot', 'SeznamBot'],
    + blackList: userAgent => (new RegExp('Googlebot|SeznamBot', 'g')).test(userAgent),
    ...
    }
    • Added new mandatory parameter action to route() method in Router of a type { type: string, event: Event|null, url: string|null }, where type attribute can take one of these values: const { REDIRECT, CLICK, POP_STATE, ERROR } = ActionTypes.
    /* ima/Router/ClientRouter.js */
    route(
    path,
    options = {},
    { event = null, type = ActionTypes.REDIRECT, url = null } = {}
    )
    • Http options withCredentials is set to false by default. You must check your HTTP CORS requests and you must set withCredentials to true for sending Cookie header.

    • New serializer settings in Jest, to configure it first add path to a newly created file jest.setup.js (located in the root directory) into setupFiles array in jest.config.json. After that add new snapshotSerializers field with ["enzyme-to-json/serializer"] value, that handles module loading, which converts Enzyme wrappers into format compatible with Jest snapshots.

    /* jest.config.json */
    "setupFiles": [
    "ima/test.js",
    "ima/polyfill/imaLoader.js",
    "ima/polyfill/imaRunner.js",
    + "<rootDir>/jest.setup.js"
    ],
    + snapshotSerializers": ["enzyme-to-json/serializer"]
    /* jest.setup.js */
    const enzyme = require('enzyme');
    const Adapter = require('enzyme-adapter-react-16');

    enzyme.configure({ adapter: new Adapter() });

    Other changes

    • IMA.js-helpers - removed throttle and debounce functions. You can replace throttle with throttle method available in UIComponentHelper in IMA.js-ui-atoms package. 73843d0
    • Updated to React 16.7, which along with new context API and other features introduced deprecation of componentWillMount, componentWillReceiveProps and componentWillUpdate lifecycle methods.
    • Upgraded to Babel 7 and latest ESLint 5, which may result on some changes specific to your application.

    New Features

    • Most notable new feature is the introduction of PageManagerHandlers which adds the possibility to customize actions like saving and restoring scroll positions, setting browser's address bar URL etc. For more information visit the documentation.
    + \ No newline at end of file diff --git a/migration/migration-17.0.0/index.html b/migration/migration-17.0.0/index.html index e563c7ae1..b8bc61acf 100644 --- a/migration/migration-17.0.0/index.html +++ b/migration/migration-17.0.0/index.html @@ -4,14 +4,14 @@ Migration 17.0.0 | IMA.js - +

    Migration 17.0.0

    IMA.js brings few major breaking changes, notably in the renaming of all packages. We've tried to make this process as easy as possible -through the provided jscodeshift transform scripts. For more information read below.

    Imports

    The ima- packages (even plugins) has been renamed to @ima/ scoped packages and ima core package has been renamed to @ima/core. The core package is now bundled with rollup, so you can no longer import a file from specific path (i.e. import GenericError from 'ima/error/GenericError'), but you can import it directly from @ima/core (i.e. import { GenericError } from '@ima/core').

    All of this can be done automatically for a whole project using following jscodeshift script.

    npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./

    Also replace paths which contain ima to @ima/core in package.json (setupFiles in jest) and server.js.

    Following packages have been renamed.

    ima-gulp-task-loader -> @ima/gulp-task-loader
    ima-gulp-tasks -> @ima/gulp-tasks
    ima-helpers -> @ima/helpers
    ima-server -> @ima/server

    Following packages have been removed.

    ima-examples
    ima-skeleton

    And as a replacement, following package has been created.

    create-ima-app

    Also all plugins have been renamed from ima-plugin-* to @ima/plugin-*.

    Context API

    IMA.js v17 no longer uses prop-types in contextTypes of React components. Instead, you should use PageContext from @ima/core. Also, prop-types has been removed from IMA.js dependencies, so if you need it for some reason, make sure it is installed as a project dependency.

    Example:

    This is original IMA.js v16 code.

    import PropTypes from 'prop-types';

    export default class MyComponent extends AbstractComponent {
    static get contextTypes() {
    return {
    $Utils: PropTypes.object,
    urlParams: PropTypes.object
    };
    }
    }

    This should be the new IMA.js v17 code.

    import { PageContext } from '@ima/core';

    export default class MyComponent extends AbstractComponent {
    static get contextType() {
    return PageContext;
    }
    }

    All of this can be done automatically for a whole project using following jscodeshift script.

    npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./

    Utils Registration

    There is a new way of defining component utils. You can no longer define oc.constant('$Utils', {...}) in app/conf/bind.js, you have to use oc.get(ComponentUtils).register({...}) instead. Also, following component utils are predefined by default, so you don't have to define them yourself.

    oc.get(ComponentUtils).register({
    $CssClasses: '$CssClasses',
    $Dictionary: Dictionary,
    $Dispatcher: Dispatcher,
    $EventBus: EventBus,
    $Helper: '$Helper',
    $Http: HttpAgent,
    $PageStateManager: PageStateManager,
    $Router: Router,
    $Settings: '$Settings',
    $Window: Window
    });

    Example:

    Following definition of utils is no longer supported.

    oc.constant('$Utils', {
    $MyCustomHelper: oc.get(MyCustomHelper),
    ...
    });

    And must be replaced with following.

    oc.get(ComponentUtils).register({
    $MyCustomHelper: MyCustomHelper,
    ...
    });

    IMA.js bundle for client/server

    IMA.js v17 comes bundled for server and client side. This means smaller bundle for clients. To benefit from this, you should update vendors in your app/build.js as following.

    let vendors = {
    - common: ['@ima/core'],
    + common: [],

    - server: [],
    + server: [{ '@ima/core': '@ima/core/dist/ima.server.cjs.js' }],

    - client: [],
    + client: [{ '@ima/core': '@ima/core/dist/ima.client.cjs.js' }],

    test: []
    };

    Language Key in Config

    Config key language (mostly used in app/config/*.js boot methods) has been renamed to $Language. You can search whole project for config.language and replace it with config.$Language, but most likely, it will be used only in app/config/settings.js.

    Hot Reload

    Hot Reload has been rewritten and published as ima plugin. Old hot reloading will no longer work. You should delete app/assets/js/hot.reload.js from your project, then install the plugin via npm install --save-dev @ima/plugin-websocket @ima/plugin-hot-reload and add following lines to your app/build.js.

    // You can add this somewhere below the vendors variable initialization
    if (
    process.env.NODE_ENV === 'dev' ||
    process.env.NODE_ENV === 'development' ||
    process.env.NODE_ENV === undefined
    ) {
    vendors.common.push('@ima/plugin-websocket');
    vendors.common.push('@ima/plugin-hot-reload');
    }

    IMA.js Plugins

    All IMA.js plugins need to be updated to the latest version. Older versions won't work.

    - +through the provided jscodeshift transform scripts. For more information read below.

    Imports

    The ima- packages (even plugins) has been renamed to @ima/ scoped packages and ima core package has been renamed to @ima/core. The core package is now bundled with rollup, so you can no longer import a file from specific path (i.e. import GenericError from 'ima/error/GenericError'), but you can import it directly from @ima/core (i.e. import { GenericError } from '@ima/core').

    All of this can be done automatically for a whole project using following jscodeshift script.

    npx jscodeshift -t node_modules/@ima/core/transform/import-v17.js --extensions jsx,js --ignore-config=.gitignore ./

    Also replace paths which contain ima to @ima/core in package.json (setupFiles in jest) and server.js.

    Following packages have been renamed.

    ima-gulp-task-loader -> @ima/gulp-task-loader
    ima-gulp-tasks -> @ima/gulp-tasks
    ima-helpers -> @ima/helpers
    ima-server -> @ima/server

    Following packages have been removed.

    ima-examples
    ima-skeleton

    And as a replacement, following package has been created.

    create-ima-app

    Also all plugins have been renamed from ima-plugin-* to @ima/plugin-*.

    Context API

    IMA.js v17 no longer uses prop-types in contextTypes of React components. Instead, you should use PageContext from @ima/core. Also, prop-types has been removed from IMA.js dependencies, so if you need it for some reason, make sure it is installed as a project dependency.

    Example:

    This is original IMA.js v16 code.

    import PropTypes from 'prop-types';

    export default class MyComponent extends AbstractComponent {
    static get contextTypes() {
    return {
    $Utils: PropTypes.object,
    urlParams: PropTypes.object
    };
    }
    }

    This should be the new IMA.js v17 code.

    import { PageContext } from '@ima/core';

    export default class MyComponent extends AbstractComponent {
    static get contextType() {
    return PageContext;
    }
    }

    All of this can be done automatically for a whole project using following jscodeshift script.

    npx jscodeshift -t node_modules/@ima/core/transform/context-api-v17.js --extensions jsx,js --ignore-config=.gitignore ./

    Utils Registration

    There is a new way of defining component utils. You can no longer define oc.constant('$Utils', {...}) in app/conf/bind.js, you have to use oc.get(ComponentUtils).register({...}) instead. Also, following component utils are predefined by default, so you don't have to define them yourself.

    oc.get(ComponentUtils).register({
    $CssClasses: '$CssClasses',
    $Dictionary: Dictionary,
    $Dispatcher: Dispatcher,
    $EventBus: EventBus,
    $Helper: '$Helper',
    $Http: HttpAgent,
    $PageStateManager: PageStateManager,
    $Router: Router,
    $Settings: '$Settings',
    $Window: Window
    });

    Example:

    Following definition of utils is no longer supported.

    oc.constant('$Utils', {
    $MyCustomHelper: oc.get(MyCustomHelper),
    ...
    });

    And must be replaced with following.

    oc.get(ComponentUtils).register({
    $MyCustomHelper: MyCustomHelper,
    ...
    });

    IMA.js bundle for client/server

    IMA.js v17 comes bundled for server and client side. This means smaller bundle for clients. To benefit from this, you should update vendors in your app/build.js as following.

    let vendors = {
    - common: ['@ima/core'],
    + common: [],

    - server: [],
    + server: [{ '@ima/core': '@ima/core/dist/ima.server.cjs.js' }],

    - client: [],
    + client: [{ '@ima/core': '@ima/core/dist/ima.client.cjs.js' }],

    test: []
    };

    Language Key in Config

    Config key language (mostly used in app/config/*.js boot methods) has been renamed to $Language. You can search whole project for config.language and replace it with config.$Language, but most likely, it will be used only in app/config/settings.js.

    Hot Reload

    Hot Reload has been rewritten and published as ima plugin. Old hot reloading will no longer work. You should delete app/assets/js/hot.reload.js from your project, then install the plugin via npm install --save-dev @ima/plugin-websocket @ima/plugin-hot-reload and add following lines to your app/build.js.

    // You can add this somewhere below the vendors variable initialization
    if (
    process.env.NODE_ENV === 'dev' ||
    process.env.NODE_ENV === 'development' ||
    process.env.NODE_ENV === undefined
    ) {
    vendors.common.push('@ima/plugin-websocket');
    vendors.common.push('@ima/plugin-hot-reload');
    }

    IMA.js Plugins

    All IMA.js plugins need to be updated to the latest version. Older versions won't work.

    + \ No newline at end of file diff --git a/migration/migration-18.0.0/index.html b/migration/migration-18.0.0/index.html index 843bd8ad4..7fb10c9a6 100644 --- a/migration/migration-18.0.0/index.html +++ b/migration/migration-18.0.0/index.html @@ -4,7 +4,7 @@ Migration 18.0.0 | IMA.js - + @@ -14,8 +14,8 @@ There was removed test env.

    Templates

    • 400, 500, spa templates are in server/template (look at create-ima-app)

    Update DocumentView

    You can remove getAsyncScripts method and body content replace with: (You have to add $Page.$Render.masterElementId property to settings.js)

     <div
    id={this.utils.$Settings.$Page.$Render.masterElementId}
    dangerouslySetInnerHTML={{ __html: this.props.page }}
    />
    {'#{revivalCache}'}
    {'#{revivalSettings}'}
    {'#{runner}'}

    Instead of app css loading use:

      {'#{styles}'}

    Assets => app/public

    Everything from folder app/public is moved to build folder into static folder.

    Styles

    • Remove files mark as FAKE FILE FOR GULP LESS
    • Move less files from assets/less to app/less
    • You have to move definition of less files pathes from build.js to "imports" - you have two options:
      • import less files per component
      • import root less file e.g. in main.js and use glob pattern to import other less files similar like it was in build.js
    • app/less/globals.less - this file is prepending to every less file so that you can import here variables, mixins, etc.
    • strictMaths is enabled

    Tests

    Add @swc/jest dependency. Add identity-obj-proxy for css/less in jest. -Replace enzyme-adapter-react-16 with @cfaester/enzyme-adapter-react-18.

    Other changes

    • Prepared for typescript

    Deleted packages

    You can remove following packages:

    • @ima/react-hooks - functionality moved to @ima/react-page-renderer
    • @ima/plugin-less-constants moved to @ima/cli-plugin-less-constants
    • @ima/plugin-hot-reload
    • @ima/plugin-websocket
    • @ima/gulp-task-loader
    • @ima/gulp-tasks

    IMA.js Plugins

    All IMA.js plugins need to be updated to the latest version. Older versions won't work.

    - +Replace enzyme-adapter-react-16 with @cfaester/enzyme-adapter-react-18.

    Other changes

    • Prepared for typescript

    Deleted packages

    You can remove following packages:

    • @ima/react-hooks - functionality moved to @ima/react-page-renderer
    • @ima/plugin-less-constants moved to @ima/cli-plugin-less-constants
    • @ima/plugin-hot-reload
    • @ima/plugin-websocket
    • @ima/gulp-task-loader
    • @ima/gulp-tasks

    IMA.js Plugins

    All IMA.js plugins need to be updated to the latest version. Older versions won't work.

    + \ No newline at end of file diff --git a/migration/migration-19.0.0/index.html b/migration/migration-19.0.0/index.html index 1168601a3..2e4acfc12 100644 --- a/migration/migration-19.0.0/index.html +++ b/migration/migration-19.0.0/index.html @@ -4,13 +4,13 @@ Migration 19.0.0 | IMA.js - +
    -

    Migration from 18.x.x to 19.0.0

    While IMA.js 19 is not as big of a release as previous major version, it brings some potential breaking changes to certain API and removes some deprecated functions. We have also managed to pack some additional new features.

    info

    In addition to new features, there have been significant updates to TypeScript types in IMA monorepo. This should allow you to write even better applications in TypeScript, while also benefit from better autocomplete in JS applications.

    Migration Guide

    The list of changes required to get your app compiled is pretty minimal, however we suggest you take a look at all potential breaking changes in the (full list of changes)[migration-19.0.0.md#breaking-changes].

    @ima/server updates

    • @ima/server now contains named exports, change following in ./server/app.js
    // from
    const imaServer = require('@ima/server')();

    // to
    const { createIMAServer } = require('@ima/server');
    const imaServer = createIMAServer();
    • Update definition of $Source, $RevivalSettings, $RevivalCache, $Runner, $Styles, $Scripts content variables in spa.ejs and DocumentView. These have been replaced by their lowerFirst counter-parts resource (now replaces $Source), revivalSettings, revivalCache, runner, styles, while $Scripts support have been dropped completely.
    • Remove urlParser middleware from app.js, it is now part of renderApp middleware as a server hook.

    Update @ima/react-page-renderer import

    Change ClientPageRenderer import from default to named import.

    // from
    import ClientPageRenderer
    from '@ima/react-page-renderer/dist/esm/client/renderer/ClientPageRenderer';

    // to
    import { ClientPageRenderer }
    from '@ima/react-page-renderer/renderer/ClientPageRenderer';

    Register new PageMetaHandler

    Add new PageMetaHandler to PageHandlerRegistry in bind.js

    oc.inject(PageHandlerRegistry, [PageNavigationHandler, PageMetaHandler, SspPageHandler]);

    Optionally remove all meta tag renders from DocumentView and spa.ejs including <title /> tag. These can be replaced with #{meta} content variable,

    Fire method params order change

    In v18 after introducting the need for a EventTarget in EventBus.fire methods, we made a mistake with the argument order. In v19 it has been moved to first position to match other event handling methods.

    // from
    this.fire('fetchDataArticles', event.target, { data: true })

    // to
    this.fire(event.target, 'fetchDataArticles', { data: true })

    Removed duplicates from HttpAgent settings:

    • headers have been moved to fetchOptions:
    // from
    $Http: {
    defaultRequestOptions: {
    headers: {
    // Set default request headers
    Accept: 'application/json',
    'Accept-Language': config.$Language,
    },
    fetchOptions: {
    mode: 'cors',
    },
    },
    }

    // to
    $Http: {
    defaultRequestOptions: {
    fetchOptions: {
    mode: 'cors',
    headers: {
    // Set default request headers
    Accept: 'application/json',
    'Accept-Language': config.$Language,
    },
    },
    },
    }

    Full list of changes

    New features

    @ima/cli

    • Added support for 3rd party source maps using source-loader, this is usefull especially in error overlay.
    • Added ability to customize open URL using --openUrl CLI argument or IMA_CLI_OPEN_URL environment variable. For more information see --openUrl.
    • Performance improvement when building CSS/LESS files (except CSS modules), on server and client bundles. This can add up to 25% built speed improvement depending on the amount of CSS files your project is using.
    • Added additional CLI output information when forcedLegacy and writeToDisk options are used.
    • Fixed manifest CSS files regexp, only files from static/css/ folder are now included in final manifest.json file.
    • Added new export for findRules, this is simple helper function you can use to extract rules from webpack config in yor plugins for easier customization.
    • Added new export for createWebpackConfig, when provided with CLI args and imaConfig, it generates webpack configurations which are then passed to webpack compiler. This can be usefull for other tooling like StoryBook, where you need to customize different webpack config with fields from the IMA app one
    • Added additional ImaConfigurationContext variables: isClientES, isClient and outputFolders.
    • Added support for prepareConfigurations CLI plugin method, which lets you customize webpack configuration contexts, before generating webpack config from them.
    • Added new cssBrowsersTarget ima.config.js settings, this allows you to easily customize postcss-preset-env browsers targets field.

    New @ima/cli@19 features

    New @ima/cli@19 features

    @ima/plugin-cli

    • Added support for source-maps, now all files transformed using swc (JS/TS) also produce .map files alongside transformed files.
    • Added ability to enable/disable source maps generation using sourceMaps option in ima-plugin.config.js configuration file.
    • Added ability to add new custom transformers using transformers option in ima-plugin.config.js configuration file.
    • When parsing configuration file the plugin now searches for ima-plugin.config.js files recursively up to filesystem root. This allows to have one custom config file for monorepositories and removes the need of duplicating same config across all package directories.

    @ima/hmr-client

    • Fixed async issue in HMR, where IMA app could be re-rendered before the old instance finished cleanup.

    @ima/core

    • Added new CancelError used for canceling running route handlers.
    • Fix window history for error action, error pages are now not added to window history.
    • Package source files now include source map files.
    • Added RouterEvents.BEFORE_LOADING_ASYNC_ROUTE and RouterEvents.AFTER_LOADING_ASYNC_ROUTE dispatcher events, which you can use to implement custom loaders when routing between async routes (or use it for any other handling).
    • All exports now use named exports (this is technically only package-wide change and does not mean nothing for the end user).
    • Added multiple new TS types, while also fixing existing types. Since rewriting IMA.js to typescript has been huge task, there may still be some type inconsistencies which we will try to fix in following releases to further improve TS experience in IMA.js ecosystem.
    • Added new onRun event to window.$IMA.Runner.
    • Add new methods isClientError() and isRedirection() to GenericError.
    • getRouteHandlersByPath() method on AbstractRouter is now public. This return's middlewares and route for given path.
    • Fixed HttpAgent types -> data in method arguments should be optional
    • Fixed missing transaction cleanup in PageStateManager
    • Fix missing optional parameters in static router that were evaluated as undefined instead of 'undefined'.
    • Added autocompletion support for language file keys in localization functions. To be able to use this function, update jsconfig.json/tsconfig.json according to the documentation (adding ./build/tmp/types/**/*" path to include field should suffice).
    • Controller and Extension event bus methods can be targeted with prefix. Prefix is set by static field in controller/extension class e.g. $name = 'ArticleController';. Event is then ArticleController.eventName:
    ./app/page/article/ArticleController.js
    class ArticleController {
    static $name = 'ArticleController';

    onExpand({ expandableId }) {
    console.log(expandableId);
    }
    }
    ./app/component/expandable/ExpandLink.jsx
    function ExpandLink() {
    onClick(event) {
    const { expandableId } = this.props;
    this.fire('ArticleController.expand', { expandableId });
    }
    }

    Router changes

    • Added middleware execution timeout => all middlewares must execute within this defined timeframe (defaults to 30s). This can be customized using $Router.middlewareTimeout app settings.
    • Router middlewares now support next callback, which when defined, has to be called, otherwise the middleware will eventually timeout and not proceed any further. This enables some additional features, where you are able to stop route processing by not calling the next function if desired.
    • Middlewares can now return object value, which will be merged to the locals object, received as a second argument in middleware function. Middlewares wich next callback function can "return" additional locals by calling next with an argument.
    router.use(async (params, locals, next) => {
    next({ counter: counter++ });
    });

    @ima/react-page-renderer

    • Package source files now include source map files.
    • Fixed once hook parametr type.
    • Moved meta tags management to new PageMetaHandler, see Seo and Meta Manager section for new updates to meta manager.
    • IMA specific React hooks have been rewritten to TypeScript.
    • Added package exports of multiple missing TS types and other interfaces (this provides better support for writing your applications in TS).

    @ima/error-overlay

    • Fixed an issue where invalid Error params caused circular dependency error.
    • Fixed an issue where errors, that occurred before error overlay is initialized were not reported to the error overlay.
    • Reduced number of levels that are expanded by default in error overlay error params view.
    • Added ability to hide/show error params, this settings is saved to local storage.

    @ima/server

    • Style content variable now automatically generates preload links for app styles.
    • Added new metric - concurrent requests to monitoring.
    • Add information about error cause in places, where we used to throw away this information.
    • Add routeName key to res.locals instead of res.$IMA, since res.$IMA should not be used anymore.
    • Added X-Request-ID to revival settings. Can be accessed through $IMA.$RequestID. This can be usefull to match same requests between client and server instances.
    • Added XSS protection to host and protocol in revival settings.
    • Add support for Client Errors and Redirects when serving static error pages.
    • Added option to force app host and protocol, using $Server.host and $Server.protocol settings in the environment.js. (These 2 values can also be functions).
    • The App error route is protected for exceeding static thresholds.
    • The Emitter event.cause is removed. The error cause is set in event.error.cause.
    • Fixed issue with dummyApp forcing 'en' language, which fails to resolve on applications with different language settings.
    • Fixed issue where server redirect showed ErrorOverlay in debug mode.
    • The instances of $Dispatcher, $Cache, $PageRenderer and $PageManager are cleared after server sending response. Clearing PageManager cause calling destroy lifecycle method of controller and extensions on server.
    • Add option to use custom manifestRequire.
    • SPA blacklist config is omitted for using degradation isSPA method when decision serving SPA page.

    create-ima-app

    • Added new typescript template, use --typescript option when generating new application.
    • Migrated from default to named exports.
    • Fixed default static path and public path settings.
    • Updated environment.js and settings.js to support new IMA19 features.

    Breaking Changes

    @ima/cli

    • Removed isESVersion ImaConfigurationContext variable (use isClientES instead).

    @ima/core

    • AbstractRouter.manage method no longer has controller and view properties in an object argument.
    • Multiple changes in router route handling and page manager with a goal of implementing ability to cancel running handlers before handling a new ones. This results in much more stable routing specifically when using async routes. Each route should now be executed "sequentially" where BEFORE/AFTER_HANDLE_ROUTE router events should always fire in correct order. Also if you quickly move between different routes, without them finishing loading, the page manager is able to cancel it's executing mid handling and continue with a new route, which results in faster and more stable routing. While this change is essentially not a breaking change, since it only changes our internal API, it could possibly result in some new behavior.
    • Removed ExtensibleError.
    • StatusCode has been renamed to HttpStatusCode.
    • $Source environment.js variable has been renamed to $Resources.
    • Removed deprecated package entry points, this includes all imports directly referencing files from ./dist/ directory. Please update your imports to the new exports fields.
    • extractParameters() function in DynamicRoute now receives additional object argument, containing query and path (not modified path) for more control over extracted parameters. The router now uses params returned from extractParameters() directly. It no longer automatically merges query params into the resulting object. If you want to preserve this behavior, merge the extracted route params with query object provided in the second argument.

    Router changes

    • Replace custom URL parsing methods in AbstractRoute, StaticRoute and DynamicRoute with combination of native URL and URLSearchParams.
    • Removed pairsToQuery, paramsToQuery, getQuery, decodeURIParameter static methods on AbstractRoute. These have been replaced with combination of native URL and URLSearchParams interfaces.
    • getTrimmedPath static method in AbstractRoute is now instance method.
    • Url query params with no value (?param=) are no longer extracted as { param: true }, but as { param: '' }. Please update your code to check for key presence in these cases rather than true value.
    • Parsing of semi-colons inside query params is not supported (as a result of using URLSearchParams)

    HttpAgent changes

    • IMA HttpAgent now removes by default all headers from request and response which is stored in Cache. You can turn off this behavior with keepSensitiveHeaders option but it is not recommended.
    • Removed support for HttpAgent options.listener (these were used mainly in plugin-xhr, which is now unsupported)
    • You can now define multiple postProcessors[] in HttpAgent options. This replaces old postProcessor option, if you are using any post processor you need to update your options to postProcessors and make sure to wrap this post processor in an array.
    • Remove older, conflicting settings of HttpAgent, withCredentials, headers, and listeners. The first two now conflict with the newer options.fetchOptions, the last one (listeners) has been removed completely. options.withCredentials and options.headers are no longer followed. Use options.fetchOptions.credentials and options.fetchOptions.headers instead. For definition, see the native Fetch API (note: for simplicity, options.fetchOptions.headers only accepts headers defined by an object, not a tuple or an instance of Headers).

    MetaManager changes

    • Rewritten meta tag management in SPA mode, all MetaManager managed tags are removed between pages while new page contains only those currently defined using setMetaParams function in app controller. This should make meta tags rendering more deterministic, while fixing situations where old meta tags might be left on the page indefinitely if not cleaner properly.
    • MetaManager getters now always return object with key=value pairs of their set value. This should make settings additional meta attributes in loops much easier (for example: getMetaProperty('og:title'); -> { property: 'property-value' });)
    • Meta values/attributes with null/undefined values are not rendered, other values are converted to string.
    • Added new iterator functions to MetaManager.
    this.#metaManager.getMetaNamesIterator();
    this.#metaManager.getMetaPropertiesIterator();
    this.#metaManager.getLinksIterator();
    • Added ability to set additional attributes for meta tags/links in meta manager:
    this.#metaManager.setLink('lcp-image', media.url, {
    'lcp-image-imagesizes': media.sizes,
    'lcp-image-imagesrcset': media.srcSet
    });

    @ima/react-page-renderer

    • Removed deprecated package entry points, this includes all imports directly referencing files from ./dist/ directory. Please update your imports to the new exports fields.
    • isSSR hook has been removed, use window.isClient() directly from useComponentUtils().
    • useSettings now returns undefined, when settings is not found when using selector namespace as an argument.
    • All exports are now named exports, you need to update import to ClientPageRenderer in bind.js to:
    • Changed signature of useWindowEvent hook, it now matches bindEventListener parameters of ima window.
    ./app/config/bind.js
    import { ClientPageRenderer } from '@ima/react-page-renderer/renderer/ClientPageRenderer';
    • Change order of method arguments in Component fire method. target has been moved to the first argument position.
    // from
    this.fire('fetchDataArticles', event.target, { data: true })

    // to
    this.fire(event.target, 'fetchDataArticles', { data: true })

    @ima/dev-utils

    @ima/server

    • Update @esmj/monitor to 0.5.0 with breaking change for returns value from subscribe method where returns subscription is object with unsubscribe method.
    • Migrated urlParser middleware to ima server BeforeRequest hook. Remove urlParser middleware from app.js, it is now part of renderApp middleware.
    • Dropped support for direct response.contentVariables mutations, use event.result and return values in CreateContentVariables event.
    • Dropped support for $Source, $RevivalSettings, $RevivalCache, $Runner, $Styles, $Scripts content variables. These have been replaced by their lowerFirst counter-parts resource (now replaces $Source), revivalSettings, revivalCache, runner, styles, while $Scripts support have been dropped completely.
    • Default resources in $Resources now produce styles and esStyles fields (should not break anything in 99% of the applications). This does not necessarily mean which should be loaded on which es version, but what bundle produced those styles. This also means that without any custom configuration, all styles should now be under esStyles key, since they are built in client.es webpack bundle. This change was made to enable built of 2 CSS bundles simliar to how we handle ES bundles. This can be enabled using @ima/cli-plugin-legacy-css.
    • The package now provides multiple additional exports using named exports, the deafult export has been replaced with named createIMAServer function.
    - +

    Migration from 18.x.x to 19.0.0

    While IMA.js 19 is not as big of a release as previous major version, it brings some potential breaking changes to certain API and removes some deprecated functions. We have also managed to pack some additional new features.

    info

    In addition to new features, there have been significant updates to TypeScript types in IMA monorepo. This should allow you to write even better applications in TypeScript, while also benefit from better autocomplete in JS applications.

    Migration Guide

    The list of changes required to get your app compiled is pretty minimal, however we suggest you take a look at all potential breaking changes in the (full list of changes)[migration-19.0.0.md#breaking-changes].

    @ima/server updates

    • @ima/server now contains named exports, change following in ./server/app.js
    // from
    const imaServer = require('@ima/server')();

    // to
    const { createIMAServer } = require('@ima/server');
    const imaServer = createIMAServer();
    • Update definition of $Source, $RevivalSettings, $RevivalCache, $Runner, $Styles, $Scripts content variables in spa.ejs and DocumentView. These have been replaced by their lowerFirst counter-parts resource (now replaces $Source), revivalSettings, revivalCache, runner, styles, while $Scripts support have been dropped completely.
    • Remove urlParser middleware from app.js, it is now part of renderApp middleware as a server hook.

    Update @ima/react-page-renderer import

    Change ClientPageRenderer import from default to named import.

    // from
    import ClientPageRenderer
    from '@ima/react-page-renderer/dist/esm/client/renderer/ClientPageRenderer';

    // to
    import { ClientPageRenderer }
    from '@ima/react-page-renderer/renderer/ClientPageRenderer';

    Register new PageMetaHandler

    Add new PageMetaHandler to PageHandlerRegistry in bind.js

    oc.inject(PageHandlerRegistry, [PageNavigationHandler, PageMetaHandler, SspPageHandler]);

    Optionally remove all meta tag renders from DocumentView and spa.ejs including <title /> tag. These can be replaced with #{meta} content variable,

    Fire method params order change

    In v18 after introducting the need for a EventTarget in EventBus.fire methods, we made a mistake with the argument order. In v19 it has been moved to first position to match other event handling methods.

    // from
    this.fire('fetchDataArticles', event.target, { data: true })

    // to
    this.fire(event.target, 'fetchDataArticles', { data: true })

    Removed duplicates from HttpAgent settings:

    • headers have been moved to fetchOptions:
    // from
    $Http: {
    defaultRequestOptions: {
    headers: {
    // Set default request headers
    Accept: 'application/json',
    'Accept-Language': config.$Language,
    },
    fetchOptions: {
    mode: 'cors',
    },
    },
    }

    // to
    $Http: {
    defaultRequestOptions: {
    fetchOptions: {
    mode: 'cors',
    headers: {
    // Set default request headers
    Accept: 'application/json',
    'Accept-Language': config.$Language,
    },
    },
    },
    }

    Full list of changes

    New features

    @ima/cli

    • Added support for 3rd party source maps using source-loader, this is usefull especially in error overlay.
    • Added ability to customize open URL using --openUrl CLI argument or IMA_CLI_OPEN_URL environment variable. For more information see --openUrl.
    • Performance improvement when building CSS/LESS files (except CSS modules), on server and client bundles. This can add up to 25% built speed improvement depending on the amount of CSS files your project is using.
    • Added additional CLI output information when forcedLegacy and writeToDisk options are used.
    • Fixed manifest CSS files regexp, only files from static/css/ folder are now included in final manifest.json file.
    • Added new export for findRules, this is simple helper function you can use to extract rules from webpack config in yor plugins for easier customization.
    • Added new export for createWebpackConfig, when provided with CLI args and imaConfig, it generates webpack configurations which are then passed to webpack compiler. This can be usefull for other tooling like StoryBook, where you need to customize different webpack config with fields from the IMA app one
    • Added additional ImaConfigurationContext variables: isClientES, isClient and outputFolders.
    • Added support for prepareConfigurations CLI plugin method, which lets you customize webpack configuration contexts, before generating webpack config from them.
    • Added new cssBrowsersTarget ima.config.js settings, this allows you to easily customize postcss-preset-env browsers targets field.

    New @ima/cli@19 features

    New @ima/cli@19 features

    @ima/plugin-cli

    • Added support for source-maps, now all files transformed using swc (JS/TS) also produce .map files alongside transformed files.
    • Added ability to enable/disable source maps generation using sourceMaps option in ima-plugin.config.js configuration file.
    • Added ability to add new custom transformers using transformers option in ima-plugin.config.js configuration file.
    • When parsing configuration file the plugin now searches for ima-plugin.config.js files recursively up to filesystem root. This allows to have one custom config file for monorepositories and removes the need of duplicating same config across all package directories.

    @ima/hmr-client

    • Fixed async issue in HMR, where IMA app could be re-rendered before the old instance finished cleanup.

    @ima/core

    • Added new CancelError used for canceling running route handlers.
    • Fix window history for error action, error pages are now not added to window history.
    • Package source files now include source map files.
    • Added RouterEvents.BEFORE_LOADING_ASYNC_ROUTE and RouterEvents.AFTER_LOADING_ASYNC_ROUTE dispatcher events, which you can use to implement custom loaders when routing between async routes (or use it for any other handling).
    • All exports now use named exports (this is technically only package-wide change and does not mean nothing for the end user).
    • Added multiple new TS types, while also fixing existing types. Since rewriting IMA.js to typescript has been huge task, there may still be some type inconsistencies which we will try to fix in following releases to further improve TS experience in IMA.js ecosystem.
    • Added new onRun event to window.$IMA.Runner.
    • Add new methods isClientError() and isRedirection() to GenericError.
    • getRouteHandlersByPath() method on AbstractRouter is now public. This return's middlewares and route for given path.
    • Fixed HttpAgent types -> data in method arguments should be optional
    • Fixed missing transaction cleanup in PageStateManager
    • Fix missing optional parameters in static router that were evaluated as undefined instead of 'undefined'.
    • Added autocompletion support for language file keys in localization functions. To be able to use this function, update jsconfig.json/tsconfig.json according to the documentation (adding ./build/tmp/types/**/*" path to include field should suffice).
    • Controller and Extension event bus methods can be targeted with prefix. Prefix is set by static field in controller/extension class e.g. $name = 'ArticleController';. Event is then ArticleController.eventName:
    ./app/page/article/ArticleController.js
    class ArticleController {
    static $name = 'ArticleController';

    onExpand({ expandableId }) {
    console.log(expandableId);
    }
    }
    ./app/component/expandable/ExpandLink.jsx
    function ExpandLink() {
    onClick(event) {
    const { expandableId } = this.props;
    this.fire('ArticleController.expand', { expandableId });
    }
    }

    Router changes

    • Added middleware execution timeout => all middlewares must execute within this defined timeframe (defaults to 30s). This can be customized using $Router.middlewareTimeout app settings.
    • Router middlewares now support next callback, which when defined, has to be called, otherwise the middleware will eventually timeout and not proceed any further. This enables some additional features, where you are able to stop route processing by not calling the next function if desired.
    • Middlewares can now return object value, which will be merged to the locals object, received as a second argument in middleware function. Middlewares wich next callback function can "return" additional locals by calling next with an argument.
    router.use(async (params, locals, next) => {
    next({ counter: counter++ });
    });

    @ima/react-page-renderer

    • Package source files now include source map files.
    • Fixed once hook parametr type.
    • Moved meta tags management to new PageMetaHandler, see Seo and Meta Manager section for new updates to meta manager.
    • IMA specific React hooks have been rewritten to TypeScript.
    • Added package exports of multiple missing TS types and other interfaces (this provides better support for writing your applications in TS).

    @ima/error-overlay

    • Fixed an issue where invalid Error params caused circular dependency error.
    • Fixed an issue where errors, that occurred before error overlay is initialized were not reported to the error overlay.
    • Reduced number of levels that are expanded by default in error overlay error params view.
    • Added ability to hide/show error params, this settings is saved to local storage.

    @ima/server

    • Style content variable now automatically generates preload links for app styles.
    • Added new metric - concurrent requests to monitoring.
    • Add information about error cause in places, where we used to throw away this information.
    • Add routeName key to res.locals instead of res.$IMA, since res.$IMA should not be used anymore.
    • Added X-Request-ID to revival settings. Can be accessed through $IMA.$RequestID. This can be usefull to match same requests between client and server instances.
    • Added XSS protection to host and protocol in revival settings.
    • Add support for Client Errors and Redirects when serving static error pages.
    • Added option to force app host and protocol, using $Server.host and $Server.protocol settings in the environment.js. (These 2 values can also be functions).
    • The App error route is protected for exceeding static thresholds.
    • The Emitter event.cause is removed. The error cause is set in event.error.cause.
    • Fixed issue with dummyApp forcing 'en' language, which fails to resolve on applications with different language settings.
    • Fixed issue where server redirect showed ErrorOverlay in debug mode.
    • The instances of $Dispatcher, $Cache, $PageRenderer and $PageManager are cleared after server sending response. Clearing PageManager cause calling destroy lifecycle method of controller and extensions on server.
    • Add option to use custom manifestRequire.
    • SPA blacklist config is omitted for using degradation isSPA method when decision serving SPA page.

    create-ima-app

    • Added new typescript template, use --typescript option when generating new application.
    • Migrated from default to named exports.
    • Fixed default static path and public path settings.
    • Updated environment.js and settings.js to support new IMA19 features.

    Breaking Changes

    @ima/cli

    • Removed isESVersion ImaConfigurationContext variable (use isClientES instead).

    @ima/core

    • AbstractRouter.manage method no longer has controller and view properties in an object argument.
    • Multiple changes in router route handling and page manager with a goal of implementing ability to cancel running handlers before handling a new ones. This results in much more stable routing specifically when using async routes. Each route should now be executed "sequentially" where BEFORE/AFTER_HANDLE_ROUTE router events should always fire in correct order. Also if you quickly move between different routes, without them finishing loading, the page manager is able to cancel it's executing mid handling and continue with a new route, which results in faster and more stable routing. While this change is essentially not a breaking change, since it only changes our internal API, it could possibly result in some new behavior.
    • Removed ExtensibleError.
    • StatusCode has been renamed to HttpStatusCode.
    • $Source environment.js variable has been renamed to $Resources.
    • Removed deprecated package entry points, this includes all imports directly referencing files from ./dist/ directory. Please update your imports to the new exports fields.
    • extractParameters() function in DynamicRoute now receives additional object argument, containing query and path (not modified path) for more control over extracted parameters. The router now uses params returned from extractParameters() directly. It no longer automatically merges query params into the resulting object. If you want to preserve this behavior, merge the extracted route params with query object provided in the second argument.

    Router changes

    • Replace custom URL parsing methods in AbstractRoute, StaticRoute and DynamicRoute with combination of native URL and URLSearchParams.
    • Removed pairsToQuery, paramsToQuery, getQuery, decodeURIParameter static methods on AbstractRoute. These have been replaced with combination of native URL and URLSearchParams interfaces.
    • getTrimmedPath static method in AbstractRoute is now instance method.
    • Url query params with no value (?param=) are no longer extracted as { param: true }, but as { param: '' }. Please update your code to check for key presence in these cases rather than true value.
    • Parsing of semi-colons inside query params is not supported (as a result of using URLSearchParams)

    HttpAgent changes

    • IMA HttpAgent now removes by default all headers from request and response which is stored in Cache. You can turn off this behavior with keepSensitiveHeaders option but it is not recommended.
    • Removed support for HttpAgent options.listener (these were used mainly in plugin-xhr, which is now unsupported)
    • You can now define multiple postProcessors[] in HttpAgent options. This replaces old postProcessor option, if you are using any post processor you need to update your options to postProcessors and make sure to wrap this post processor in an array.
    • Remove older, conflicting settings of HttpAgent, withCredentials, headers, and listeners. The first two now conflict with the newer options.fetchOptions, the last one (listeners) has been removed completely. options.withCredentials and options.headers are no longer followed. Use options.fetchOptions.credentials and options.fetchOptions.headers instead. For definition, see the native Fetch API (note: for simplicity, options.fetchOptions.headers only accepts headers defined by an object, not a tuple or an instance of Headers).

    MetaManager changes

    • Rewritten meta tag management in SPA mode, all MetaManager managed tags are removed between pages while new page contains only those currently defined using setMetaParams function in app controller. This should make meta tags rendering more deterministic, while fixing situations where old meta tags might be left on the page indefinitely if not cleaner properly.
    • MetaManager getters now always return object with key=value pairs of their set value. This should make settings additional meta attributes in loops much easier (for example: getMetaProperty('og:title'); -> { property: 'property-value' });)
    • Meta values/attributes with null/undefined values are not rendered, other values are converted to string.
    • Added new iterator functions to MetaManager.
    this.#metaManager.getMetaNamesIterator();
    this.#metaManager.getMetaPropertiesIterator();
    this.#metaManager.getLinksIterator();
    • Added ability to set additional attributes for meta tags/links in meta manager:
    this.#metaManager.setLink('lcp-image', media.url, {
    'lcp-image-imagesizes': media.sizes,
    'lcp-image-imagesrcset': media.srcSet
    });

    @ima/react-page-renderer

    • Removed deprecated package entry points, this includes all imports directly referencing files from ./dist/ directory. Please update your imports to the new exports fields.
    • isSSR hook has been removed, use window.isClient() directly from useComponentUtils().
    • useSettings now returns undefined, when settings is not found when using selector namespace as an argument.
    • All exports are now named exports, you need to update import to ClientPageRenderer in bind.js to:
    • Changed signature of useWindowEvent hook, it now matches bindEventListener parameters of ima window.
    ./app/config/bind.js
    import { ClientPageRenderer } from '@ima/react-page-renderer/renderer/ClientPageRenderer';
    • Change order of method arguments in Component fire method. target has been moved to the first argument position.
    // from
    this.fire('fetchDataArticles', event.target, { data: true })

    // to
    this.fire(event.target, 'fetchDataArticles', { data: true })

    @ima/dev-utils

    @ima/server

    • Update @esmj/monitor to 0.5.0 with breaking change for returns value from subscribe method where returns subscription is object with unsubscribe method.
    • Migrated urlParser middleware to ima server BeforeRequest hook. Remove urlParser middleware from app.js, it is now part of renderApp middleware.
    • Dropped support for direct response.contentVariables mutations, use event.result and return values in CreateContentVariables event.
    • Dropped support for $Source, $RevivalSettings, $RevivalCache, $Runner, $Styles, $Scripts content variables. These have been replaced by their lowerFirst counter-parts resource (now replaces $Source), revivalSettings, revivalCache, runner, styles, while $Scripts support have been dropped completely.
    • Default resources in $Resources now produce styles and esStyles fields (should not break anything in 99% of the applications). This does not necessarily mean which should be loaded on which es version, but what bundle produced those styles. This also means that without any custom configuration, all styles should now be under esStyles key, since they are built in client.es webpack bundle. This change was made to enable built of 2 CSS bundles simliar to how we handle ES bundles. This can be enabled using @ima/cli-plugin-legacy-css.
    • The package now provides multiple additional exports using named exports, the deafult export has been replaced with named createIMAServer function.
    + \ No newline at end of file diff --git a/plugins/available-plugins/index.html b/plugins/available-plugins/index.html index 25ca44d8f..4cdd4266a 100644 --- a/plugins/available-plugins/index.html +++ b/plugins/available-plugins/index.html @@ -4,14 +4,14 @@ Existing plugins | IMA.js - +

    Existing plugins

    We've already described a way to create your own IMA.js plugins -through a very simple interface. Now we would like to talk about IMA.js-plugins monorepo that already contains variety of plugins that covers many of the common use cases.

    IMA.js-plugins

    Each plugin in this repository is thoroughly tested and maintained, so it always works with the most up to date IMA.js version. We, here at Seznam.cz use it daily in production on many of our projects, so don't worry about using them safely in the production environment.

    Without further ado, let's quickly describe in this compact list what each plugin does and when you would want to use them:

    note

    This list is updated manually, so there can be situations where it doesn't match on 100% what is currently present in the monorepository itself.

    - +through a very simple interface. Now we would like to talk about IMA.js-plugins monorepo that already contains variety of plugins that covers many of the common use cases.

    IMA.js-plugins

    Each plugin in this repository is thoroughly tested and maintained, so it always works with the most up to date IMA.js version. We, here at Seznam.cz use it daily in production on many of our projects, so don't worry about using them safely in the production environment.

    Without further ado, let's quickly describe in this compact list what each plugin does and when you would want to use them:

    note

    This list is updated manually, so there can be situations where it doesn't match on 100% what is currently present in the monorepository itself.

    + \ No newline at end of file diff --git a/plugins/plugin-api/index.html b/plugins/plugin-api/index.html index b46048943..2d8d1ad13 100644 --- a/plugins/plugin-api/index.html +++ b/plugins/plugin-api/index.html @@ -4,7 +4,7 @@ Plugins API | IMA.js - + @@ -12,8 +12,8 @@

    Plugins API

    IMA.js development stack offers built-in support for plugins. Writing plugins for IMA.js is really simple. It basically comes to creating an ordinary npm package and using pluginLoader.register method to hook into IMA.js application environment using certain functions.

    info

    In situations where you don't need to hook into IMA.js app environment from within your plugin (you're for example just exporting some interface), you don't need call this registration method as it servers no purpose.

    Plugin registration

    As mentioned above, the plugin registration is done from within your npm package entry point using pluginLoader.register method:

    import { pluginLoader } from '@ima/core';
    import Service from './service';

    pluginLoader.register('my-ima-plugin', ns => {
    ns.set('my.ima.plugin.Service', Service);
    });

    The register method expects 2 arguments, first is name of your plugin (this is used strictly for debugging purposes, however it is required) and callback registration function which receives Namespace as one and only argument, that you can use to specify to which namespace this plugin should be bound.

    Plugin bootstrap functions

    The registration function can additionally return an object with additional callback functions. These allow you to further bootstrap your plugin. All are however optional, meaning you can define any combination of these or don't return anything.

    import { pluginLoader } from '@ima/core';

    pluginLoader.register('my-ima-plugin', ns => {
    return {
    initBind: (ns, oc, config) => {},
    initServices: (ns, oc, config) => {},
    initSettings: (ns, oc, config) => {}
    }
    });

    initBind

    initBind(ns: Namespace, oc: ObjectContainer, config: Config['bind'], isDynamicallyLoaded = false)

    This function has the same interface as a function exported in bind.js of your IMA.js application and also serves the same purpose. This is the place where you would want to initialize your custom constants and bindings and assign them to the ObjectContainer.

    initServices

    initServices(ns: Namespace, oc: ObjectContainer, config: Config['services'], isDynamicallyLoaded = false)

    Similarly to initBind, this is equivalent to a function exported by services.js file in your application.

    initSettings

    initSettings(ns: Namespace, oc: ObjectContainer, config: Config['settings'], isDynamicallyLoaded = false)

    You can probably already see the pattern here. This function should return an object with settings, with the same structure as function in settings.js file does.

    These settings are then merged with your application settings a possible conflicts are overridden with the application settings. This allows you to define defaults for your plugin, which can be easily overridden in your application.

    Examples

    Putting it all together, your main file in your npm package could look something like this (borrowing contents of main.js from our @ima/plugin-useragent:

    import { pluginLoader } from '@ima/core';
    import PlatformJS from 'platform';

    import UserAgent from './AbstractUserAgent.js';
    import ClientUserAgent from './ClientUserAgent.js';
    import ServerUserAgent from './ServerUserAgent.js';

    pluginLoader.register('@ima/plugin-useragent', () => {
    return {
    initBind: (ns, oc) => {
    if (oc.get('$Window').isClient()) {
    oc.provide(UserAgent, ClientUserAgent, [PlatformJS, '$Window']);
    } else {
    oc.provide(UserAgent, ServerUserAgent, [PlatformJS, '$Request']);
    }
    },
    initServices: (ns, oc) => {
    oc.get(UserAgent).init();
    },
    };
    });

    export { ClientUserAgent, ServerUserAgent, UserAgent, PlatformJS };

    Dynamically imported plugins and tree shaking

    When the plugin is imported dynamically and initialized lazily, you receive isDynamicallyLoaded = true as the last argument in the registration bootstrap functions. This can help you in certain situations where you need to know when the plugin was initialized.

    The bootstrap process works the same way as with plugins initialized upon application startup, meaning all plugin settings are still overwritten with possible overrides in the application settings. There's however one caveat with the ObjectContainer that you need to pay attention to.

    danger

    When using string syntax to get certain settings in the $dependencies field:

    static get $dependencies() {
    return ['$Settings.myPlugin.repeatCount'];
    };

    constructor(repeatCount) {
    this.repeatCount = repeatCount;
    }

    fn() {
    this.repeatXTimes(this.repeatCount);
    }

    This won't be updated with possible plugin defaults when it get's loaded. In order to prevent this issue, you need to access whole settings object which will get updated values:

    static get $dependencies() {
    return ['$Settings'];
    };

    myFUnction(settings) {
    this.settings = settings;
    }

    fn() {
    this.repeatXTimes(settings?.myPlugin?.repeatCount);
    }

    Conclusion

    As you can see, creating IMA.js plugin is very easy. You can always check our IMA.js-plugins monorepo to take a look at many other already -existing plugins and how they're implemented, which we describe more in detail in the documentation.

    - +existing plugins and how they're implemented, which we describe more in detail in the documentation.

    + \ No newline at end of file diff --git a/search/index.html b/search/index.html index 8d2e7c4e5..c0f430aa8 100644 --- a/search/index.html +++ b/search/index.html @@ -4,13 +4,13 @@ Search the documentation | IMA.js - +

    Search the documentation

    - + \ No newline at end of file diff --git a/tutorial/adding-some-state/index.html b/tutorial/adding-some-state/index.html index 8a44b14c5..1007b1ac7 100644 --- a/tutorial/adding-some-state/index.html +++ b/tutorial/adding-some-state/index.html @@ -4,7 +4,7 @@ Adding Some State | IMA.js - + @@ -129,8 +129,8 @@ stop their propagation.

    Note that events distributed using the Dispatcher are useful only in very specific use-cases, so the Dispatcher logs a warning to the console if there are no listeners registered for the fired event in order to notify you of -possible typos in event names.

    As always, you can learn more about EventBus and Dispatcher in the documentation

    - +possible typos in event names.

    As always, you can learn more about EventBus and Dispatcher in the documentation

    + \ No newline at end of file diff --git a/tutorial/fetching-data/index.html b/tutorial/fetching-data/index.html index 537bc588e..64df24177 100644 --- a/tutorial/fetching-data/index.html +++ b/tutorial/fetching-data/index.html @@ -4,7 +4,7 @@ Fetching Data | IMA.js - + @@ -107,8 +107,8 @@ and sends the serialized cache to the client. The cache is then deserialized at the client-side, so the request to http://localhost:3001/static/static/public/posts.json we do in our post resource will -be resolved from the cache, leading to no additional HTTP request being made.

    - +be resolved from the cache, leading to no additional HTTP request being made.

    + \ No newline at end of file diff --git a/tutorial/final-polish/index.html b/tutorial/final-polish/index.html index fdb13492e..aa5d5908e 100644 --- a/tutorial/final-polish/index.html +++ b/tutorial/final-polish/index.html @@ -4,7 +4,7 @@ Final Polish | IMA.js - + @@ -168,8 +168,8 @@ you liked the journey and are happy with what you've learned here.

    From now I suggest to take a look at our documentation which goes into greater detail in describing each component of IMA.js development stack or take a direct look at the API.

    If you see any improvements that could be made to this tutorial, or have found any mistakes, please let us know by creating issue in our IMA.js monorepo, -or even better, creating PR.

    I bid you farewell!

    - +or even better, creating PR.

    I bid you farewell!

    + \ No newline at end of file diff --git a/tutorial/introduction/index.html b/tutorial/introduction/index.html index e74c631d6..a0ad477d3 100644 --- a/tutorial/introduction/index.html +++ b/tutorial/introduction/index.html @@ -4,7 +4,7 @@ Introduction | IMA.js - + @@ -47,8 +47,8 @@ rendered to HTML.
  • The server contains the application logic of the HTTP server serving our application.
  • Finally, the build directory is used as an output directory for the built application and its resources.
  • For more information see the Application Structure -section in the documentation.

    - +section in the documentation.

    + \ No newline at end of file diff --git a/tutorial/static-view/index.html b/tutorial/static-view/index.html index 0ce261d41..16aa8397c 100644 --- a/tutorial/static-view/index.html +++ b/tutorial/static-view/index.html @@ -4,7 +4,7 @@ Static View | IMA.js - + @@ -105,8 +105,8 @@ ns.namespace('namespace name goes here'). The second snippet binds the class, constant or value created in the file to the namespace.

    If you're using version 15 and above you can safely remove deprecated namespaces and replace them with ES2015 import -and export.

    - +and export.

    + \ No newline at end of file diff --git a/tutorial/writing-posts/index.html b/tutorial/writing-posts/index.html index be8037a34..171340a6a 100644 --- a/tutorial/writing-posts/index.html +++ b/tutorial/writing-posts/index.html @@ -4,7 +4,7 @@ Writing Posts | IMA.js - + @@ -93,8 +93,8 @@ modifications of data passed to or received from our mock server won't modify the internal state or data returned by other calls to our methods.

    To wire up our HTTP mock into our application, we need to update the dependencies of the app/model/post/PostResource.js:

    import PostFactory from './PostFactory';
    import MockHttpAgent from 'app/mock/MockHttpAgent';

    export default class PostResource {
    static get $dependencies() {
    return [MockHttpAgent, PostFactory];
    }

    ...
    }

    Go ahead and check the result in the browser, you will now be able to write new posts to our guestbook (which will disappear once you reload the page, since we -keep the posts only in our HTTP mock).

    - +keep the posts only in our HTTP mock).

    + \ No newline at end of file