Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Node + Angular Universal SSR: how to set the device-width when rendering a page

I'm looking for a way to set the device-width for the server side render with Angular Universal so I control whether the prerendered page is in mobile or desktop layout.

I'm using the core ngExpressEngine to do the rendering (pretty much the same as the universal starter.

const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));
like image 984
JayChase Avatar asked Nov 07 '22 09:11

JayChase


1 Answers

UPDATE: Gave up using jsdom as explained before, because it executes the scripts on the rendered page, which is not intended. Possibly can be tuned with runScripts option, still would receive performance hit. Regex replace on rendered strings is much faster and safer. Below sample updated to reflect it.


Today I came across same issue. Having Angular application with Universal support enabled and @angular/flex-layout.

When this application is rendered on browser, ObservableMedia of @angular/flex-layout properly reports the media for instance:

// browser side MediaChange event
{
  matches: true,
  mediaQuery: "(min-width: 1280px) and (max-width: 1919px)",
  mqAlias: "lg",
  property: "",
  suffix: "Lg"
}

When same application is rendered on server:

// server side MediaChange event
{
  matches: true,
  mediaQuery: "all",
  mqAlias: "",
  property: "",
  suffix: ""
}

So basically, server side does not aware by default of client's media parameters, that is understandable.

If you have some mechanism of passing client's device width (for instance via cookies, personalization API etc.), then you can use jsdom regex string replace to modify rendered document. Roughly it will look like this:

// DON'T USE JSDOM, BECAUSE IT WILL EXECUTE SCRIPTS WHICH IS NOT INTENDED
// this probably may cache generated htmls
// because they are limited by the number of media queries
/*
function updateMetaViewport(html: string, deviceWidth?: number): string {
  const dom = new JSDOM(html);
  const metaViewport = dom.window.document.head.querySelector<HTMLMetaElement>('meta[name="viewport"]');
  // if deviceWidth is not specified use default 'device-width'
  // needed for both default case, and relaxing rendered html
  metaViewport.content = `width=${deviceWidth ? deviceWidth : 'device-width'}, initial-scale=1`;
  return dom.serialize();     
}
*/

// INSTEAD REGEX WILL BE SIMPLIER AND FASTER FOR THIS TASK
// use regex string replace to update meta viewport tag
// can be optimized further by splitting html into two pieces
// and running regex replace over first part, and then concatenate
// replaced and remaining (if rendered html is large enough)
function updateMetaViewport(html: string, deviceWidth?: number, deviceHeight?: number): string {
  const width = `width=${deviceWidth ? deviceWidth : 'device-width'}`;
  const height = deviceHeight ? `, height=${deviceHeight}` : '';
  const content = `${width}${height}, initial-scale=1`;
  const replaced = html.replace(
    /<head>((?:.|\n|\r)+?)<meta name="viewport" content="(.*)">((?:.|\n|\r)+?)<\/head>/i,
    `<head>$1<meta name="viewport" content="${content}">$3</head>`
  );
  return replaced;
}

router.get('*', (req, res) => {

  // where it is provided from is out of scope of this question
  const userDeviceWidth = req.userDeviceWidth;
  const userDeviceHeight = req.userDeviceHeight;
  // then we need to set viewport width in html
  const document = updateMetaViewport(indexHtmlDocument, userDeviceWidth, userDeviceHeight);

  res.render('index.html', {
    bootstrap: AppServerModuleNgFactory,
    providers: [provideModuleMap(LAZY_MODULE_MAP)],
    url: req.url,
    document,
    req,
    res
  }, (err, html) => {
    if (err) {
      res.status(500).send(`Internal Server Error: ${err.name}: ${err.message}`);
    } else {
      // once rendered, we need to refine the view port to default
      // other wise viewport looses its responsiveness
      const relaxViewportDocument = updateMetaViewport(html);
      res.status(200).send(relaxViewportDocument);
    }
  });
});

Then server side rendering in terms of @angular/flex-layout will be according:

{
  matches: true,
  mediaQuery: '(min-width: 600px) and (max-width: 959px)',
  mqAlias: 'sm',
  suffix: 'Sm',
  property: ''
}

Which is correct and more advantageous, because styles, layouts of responsive components will be exactly as client expects.

like image 187
muradm Avatar answered Nov 15 '22 08:11

muradm