├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.mjs ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .verdaccio └── config.yml ├── .vscode └── extensions.json ├── README.md ├── common-issues.md ├── nx.json ├── package.json ├── packages ├── .gitkeep ├── add-angular-to-qwik │ ├── .eslintrc.json │ ├── README.md │ ├── bin │ │ ├── add-angular-to-qwik.ts │ │ └── index.ts │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── qwik-angular │ ├── .eslintrc.json │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ ├── index.qwik.ts │ ├── index.ts │ ├── lib │ │ ├── client.ts │ │ ├── extract-projectable-nodes.ts │ │ ├── qwikify.tsx │ │ ├── server.tsx │ │ ├── slot.ts │ │ ├── types.ts │ │ ├── vite-plugin │ │ │ └── vite.ts │ │ └── wake-up-signal.ts │ ├── root.tsx │ └── vite.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── project.json ├── tools ├── scripts │ └── publish.mjs └── tsconfig.tools.json └── tsconfig.base.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'scope-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'qwik-angular', 9 | 'add-angular-to-qwik', 10 | 'repo', // anything related to managing the repo itself 11 | ], 12 | ], 13 | 'scope-empty': [2, 'never'], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | vite.config.ts -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": { 28 | "@typescript-eslint/no-explicit-any": "off" 29 | } 30 | }, 31 | { 32 | "files": ["*.js", "*.jsx"], 33 | "extends": ["plugin:@nx/javascript"], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | # Needed for nx-set-shas within nx-cloud-main.yml, when run on the main branch 10 | permissions: 11 | actions: read 12 | contents: read 13 | 14 | jobs: 15 | main: 16 | name: Nx Cloud - Main Job 17 | uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.13.1 18 | with: 19 | working-directory: '.' 20 | main-branch-name: master 21 | number-of-agents: 3 22 | init-commands: | 23 | pnpm exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=3 24 | parallel-commands: | 25 | pnpm exec nx-cloud record -- pnpm exec nx format:check 26 | parallel-commands-on-agents: | 27 | pnpm exec nx affected --target=lint --parallel=3 28 | pnpm exec nx affected --target=test --parallel=3 --ci --code-coverage 29 | pnpm exec nx affected --target=build --parallel=3 30 | 31 | agents: 32 | name: Nx Cloud - Agents 33 | uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.13.1 34 | with: 35 | number-of-agents: 3 36 | working-directory: '.' 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .angular 42 | 43 | .nx/cache -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm exec commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm exec lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.mjs: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import { workspaceRoot } from '@nx/devkit'; 3 | 4 | export default { 5 | '**/*': (files) => 6 | `pnpx nx format:write --files=${files 7 | .map((f) => relative(workspaceRoot, f)) 8 | .join(',')}`, 9 | '{apps,libs,packages,tools}/**/*.{ts,tsx,js,jsx}': 'eslint', 10 | }; 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | .angular 5 | 6 | /.nx/cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.verdaccio/config.yml: -------------------------------------------------------------------------------- 1 | # path to a directory with all packages 2 | storage: ../tmp/local-registry/storage 3 | 4 | # a list of other known repositories we can talk to 5 | uplinks: 6 | npmjs: 7 | url: https://registry.npmjs.org/ 8 | cache: true 9 | yarn: 10 | url: https://registry.yarnpkg.com 11 | cache: true 12 | 13 | packages: 14 | '**': 15 | # give all users (including non-authenticated users) full access 16 | # because it is a local registry 17 | access: $all 18 | publish: $all 19 | unpublish: $all 20 | 21 | # if package is not available locally, proxy requests to npm registry 22 | proxy: npmjs 23 | 24 | # log settings 25 | logs: 26 | type: stdout 27 | format: pretty 28 | level: http 29 | 30 | publish: 31 | allow_offline: true # set offline to true to allow publish offline 32 | 33 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qwik Angular 2 | 3 | QwikAngular allows you to use Angular components in Qwik, including the whole ecosystem of component libraries. 4 | 5 | ## Installation 6 | 7 | Inside your Qwik app run: 8 | 9 | ```shell 10 | npx add-angular-to-qwik@latest 11 | ``` 12 | 13 | If you don't have a Qwik app yet, then you need to [create one first](../../../docs/getting-started/index.mdx), then, follow the instructions and run the command add Angular to your app. 14 | 15 | ```shell 16 | npm create qwik@latest 17 | cd to-my-app 18 | npx add-angular-to-qwik@latest 19 | ``` 20 | 21 | ## Usage 22 | 23 | The @builder.io/qwik-angular package exports the qwikify$() function that lets you convert Angular components into Qwik components, that you can use across your application. 24 | 25 | Angular and Qwik components can not be mixed in the same file, if you check your project right after running the installation command, you will see a new folder src/integrations/angular/components, from now on, all your Angular components will live there. Qwikified components are declared and exported from src/integrations/angular/index.ts file, it is important to not place Qwik code in the Angular component files. 26 | 27 | ## Limitations 28 | 29 | ## Defining A Component 30 | 31 | The Qwik Angular integration **only** supports rendering standalone components. You can still compose required UIs with Angular code that uses modules by wrapping it inside standalone components: 32 | 33 | ```ts 34 | import { NgIf } from '@angular/common'; 35 | import { Component, Input } from '@angular/core'; 36 | import { MatButtonModule } from '@angular/material/button'; 37 | 38 | @Component({ 39 | selector: 'app-hello', 40 | standalone: true, 41 | imports: [NgIf, MatButtonModule], 42 | template: ` 43 |

Hello from Angular!!

44 | 45 |

{{ helpText }}

46 | 47 | 48 | `, 49 | }) 50 | export class HelloComponent { 51 | @Input() helpText = 'help'; 52 | 53 | show = false; 54 | 55 | toggle() { 56 | this.show = !this.show; 57 | } 58 | } 59 | ``` 60 | 61 | ### Adding types 62 | 63 | Angular defines inputs and outputs of the component using decorators, so it's not possible to deduce them as interface properties for the qwikified component to be used as JSX element. This package provides utility types `QwikifiedComponentProps` and `WithRequiredProps` to simplify the creation of typed qwikified components. 64 | 65 | `QwikifiedComponentProps` utility expects the list of keys that represent inputs and outputs of your component. It does the following: 66 | 67 | - validates that provided keys exist within the component 68 | - extracts types of the provided keys for inputs 69 | - converts outputs to the handlers that can be used with JSX syntax (e.g. `EventEmitter` is converted to `(value: string) => void`) 70 | - adds `$` suffix for outputs, which basically [lets Qwik treat them as QRLs](https://qwik.builder.io/docs/advanced/dollar) 71 | 72 | Here's an example of how it works: 73 | 74 | ```ts 75 | @Component({..}) 76 | export class InputComponent { 77 | @Input() theme: 'primary' | 'accent' | 'warn' = 'primary'; 78 | @Input() placeholder: string; 79 | @Output() changed = new EventEmitter(); 80 | } 81 | 82 | type InputComponentInputs = 'theme' | 'placeholder'; 83 | 84 | type InputComponentOutputs = 'changed'; 85 | 86 | // InputComponentProps is the interface that you can export along with your qwikified component to be used elsewhere 87 | export type InputComponentProps = QwikifiedComponentProps< 88 | InputComponent, 89 | InputComponentInputs, // inputs of the "InputComponent" 90 | InputComponentProps // outputs of the "InputComponent" 91 | >; 92 | 93 | // The final type will look like 94 | interface FinalInputTypeSample { 95 | theme?: 'primary' | 'accent' | 'warn'; 96 | placeholder?: string; 97 | changed$?: (value: string) => void; // notice that "changed" output got a "$" suffix! 98 | } 99 | // qwikify it later as follows 100 | export const MyNgInput = qwikify$(InputComponent); 101 | 102 | // additionally you can mark types as required using "WithRequiredProps" util 103 | type RequiredInputProps = 'theme'; 104 | export type RequiredInputComponentProps = WithRequiredProps; 105 | 106 | // The assembled type will have "theme" as a required property this time 107 | interface FinalInputTypeRequiredSample { 108 | theme: 'primary' | 'accent' | 'warn'; // <= became required! 109 | placeholder?: string; 110 | changed$?: (value: string) => void; 111 | } 112 | ``` 113 | 114 | ### Every qwikified Angular component is isolated 115 | 116 | Each instance of a qwikified Angular component becomes an independent Angular app. Fully isolated. 117 | 118 | ```tsx 119 | export const AngularHelloComponent = qwikify$(HelloComponent); 120 | ; 121 | ``` 122 | 123 | - Each `AngularHelloComponent` is a fully isolated Angular application, with its own state, lifecycle, etc. 124 | - Styles will be duplicated 125 | - State will not be shared. 126 | - Islands will hydrate independently 127 | 128 | ### Use `qwikify$()` as a migration strategy 129 | 130 | Using Angular components in Qwik is a great way to migrate your application to Qwik, but it's not a silver bullet, you will need to rewrite your components to take advantage of Qwik's features. 131 | 132 | It's also a great way to enjoy the Angular ecosystem. 133 | 134 | > Don't abuse of `qwikify$()` to build your own application, all performance gains will be lost. 135 | 136 | ### Build wide islands, not leaf nodes 137 | 138 | For example, if you need to use several Angular components, to build a list, don't qwikify each individual component, instead, build the whole list as a single qwikified Angular component. 139 | 140 | #### GOOD: Wide island 141 | 142 | A single qwikified component, with all the Angular components inside. Styles will not be duplicated, and context and theming will work as expected. 143 | 144 | ```ts 145 | // folder.component.ts 146 | import List from './list.component'; 147 | import ListItem from './list-item.component'; 148 | import ListItemText from './list-item-text.component'; 149 | import ListItemAvatar from './list-item-avatar.component'; 150 | import Avatar from './avatar.component'; 151 | import Icon from './icon.component'; 152 | 153 | // Qwikify the whole list 154 | @Component({ 155 | standalone: true, 156 | imports: [List, ListItem, ListItemText, ListItemAvatar, Avatar, Icon], 157 | template: ` 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | `, 185 | }) 186 | export class FolderList {} 187 | ``` 188 | 189 | #### BAD: Leaf nodes 190 | 191 | Leaf nodes are qwikified independently, effectively rendering dozens of nested Angular applications, each fully isolated from the others, and styles being duplicated. 192 | 193 | ```tsx 194 | import List from './list.component'; 195 | import ListItem from './list-item.component'; 196 | import ListItemText from './list-item-text.component'; 197 | import ListItemAvatar from './list-item-avatar.component'; 198 | import Avatar from './avatar.component'; 199 | import Icon from './icon.component'; 200 | 201 | export const AngularList = qwikify$(List); 202 | export const AngularListItem = qwikify$(ListItem); 203 | export const AngularListItemText = qwikify$(ListItemText); 204 | export const AngularListItemAvatar = qwikify$(ListItemAvatar); 205 | export const AngularAvatar = qwikify$(Avatar); 206 | export const AngularIcon = qwikify$(Icon); 207 | ``` 208 | 209 | ```tsx 210 | // Qwik component using dozens of nested Angular islands 211 | // Each Angular-* is an independent Angular application 212 | export const FolderList = component$(() { 213 | return ( 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | ); 241 | }); 242 | ``` 243 | 244 | ## Adding interactivity 245 | 246 | In order to add interactivity it is required to hydrate the Angular application. Angular uses [destructive hydration](https://blog.angular.io/angulars-vision-for-the-future-3cfca5e7b448), which means components are rendered on the server and then are fully recreated on the client. This [adds a massive overhead](https://www.builder.io/blog/hydration-is-pure-overhead) and making sites slow. 247 | 248 | Qwik allows you decide when to hydrate your components, by using the `client:` JSX properties, this technique is commonly called partial hydration, popularized by [Astro](https://astro.build/). 249 | 250 | ```diff 251 | export default component$(() => { 252 | return ( 253 | <> 254 | - 255 | + 256 | 257 | ); 258 | }); 259 | ``` 260 | 261 | Qwik comes with different strategies out of the box: 262 | 263 | ### `client:load` 264 | 265 | The component eagerly hydrates when the document loads. 266 | 267 | ```tsx 268 | 269 | ``` 270 | 271 | **Use case:** Immediately-visible UI elements that need to be interactive as soon as possible. 272 | 273 | ### `client:idle` 274 | 275 | The component eagerly hydrates when the browser first become idle, ie, when everything important as already run before. 276 | 277 | ```tsx 278 | 279 | ``` 280 | 281 | **Use case:** Lower-priority UI elements that don’t need to be immediately interactive. 282 | 283 | ### `client:visible` 284 | 285 | The component eagerly hydrates when it becomes visible in the viewport. 286 | 287 | ```tsx 288 | 289 | ``` 290 | 291 | **Use case:** Low-priority UI elements that are either far down the page (“below the fold”) or so resource-intensive to load that you would prefer not to load them at all if the user never saw the element. 292 | 293 | ### `client:hover` 294 | 295 | The component eagerly hydrates when the mouse is over the component. 296 | 297 | ```tsx 298 | 299 | ``` 300 | 301 | **Use case:** Lowest-priority UI elements which interactivity is not crucial, and only needs to run in desktop. 302 | 303 | ### `client:signal` 304 | 305 | This is an advanced API that allows to hydrate the component whenever the passed signal becomes `true`. 306 | 307 | ```tsx 308 | export default component$(() => { 309 | const hydrateAngular = useSignal(false); 310 | return ( 311 | <> 312 | 315 | 316 | 317 | ); 318 | }); 319 | ``` 320 | 321 | This effectively allows you to implement custom strategies for hydration. 322 | 323 | ### `client:event` 324 | 325 | The component eagerly hydrates when specified DOM events are dispatched. 326 | 327 | ```tsx 328 | 329 | ``` 330 | 331 | ### `client:only` 332 | 333 | When `true`, the component will not run in SSR, only in the browser. 334 | 335 | ```tsx 336 | 337 | ``` 338 | 339 | ## Listening to Angular events 340 | 341 | Events in Angular are propagated as component outputs: 342 | 343 | ```html 344 | 345 | ; 346 | ``` 347 | 348 | The `qwikify()` function will convert all outputs into properties with functional handlers 349 | 350 | ```tsx 351 | import { Slider } from './components'; 352 | import { qwikify$ } from '@builder.io/qwik-angular'; 353 | const AngularSlider = qwikify$(Slider); 354 | console.log('value changed')} />; 355 | ``` 356 | 357 | > Notice that we use the `client:visible` property to eagerly hydrate the component, otherwise the component would not be interactive and the events would never be dispatched. 358 | 359 | ## Host element 360 | 361 | When wrapping an Angular component with `qwikify$()`, under the hood, a new DOM element is created, such as: 362 | 363 | ```html 364 | 365 | 366 | 367 | ``` 368 | 369 | > Notice, that the tag name of the wrapper element is configurable via `tagName`: `qwikify$(AngularCmp, { tagName: 'my-ng' })`. 370 | 371 | ### Listen to DOM events without hydration 372 | 373 | The host element is not part of Angular, meaning that hydration is not necessary to listen for events, in order to add custom attributes and events to the host element, you can use the `host:` prefix in the JSX properties, such as: 374 | 375 | ```tsx 376 | { 378 | console.log('click an Angular component without hydration!!'); 379 | }} 380 | /> 381 | ``` 382 | 383 | This will effectively allow you to respond to a click in an Angular button without downloading a single byte of Angular code. 384 | 385 | Happy hacking! 386 | -------------------------------------------------------------------------------- /common-issues.md: -------------------------------------------------------------------------------- 1 | Integration between angular and qwik involves large amount of dependencies and sometimes something is not working properly with a weird undebuggable error. This document describes some of them and possible reasons 2 | 3 | ## NG0203: inject() must be called from an injection context 4 | 5 | This can be caused by a mismatch between the version of @angular/_ packages and the version @qwikdev/qwik-angular depends on. Typically, you'd see `node_modules/@angular/_`packages along with`node_modules/@qwikdev/qwik-angular/node_modules/@angular/\*` 6 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "targetDefaults": { 7 | "build": { 8 | "dependsOn": ["^build"], 9 | "inputs": ["production", "^production"], 10 | "cache": true 11 | }, 12 | "lint": { 13 | "inputs": [ 14 | "default", 15 | "{workspaceRoot}/.eslintrc.json", 16 | "{workspaceRoot}/.eslintignore" 17 | ], 18 | "cache": true 19 | }, 20 | "test": { 21 | "inputs": ["default", "^production"], 22 | "cache": true 23 | }, 24 | "e2e": { 25 | "cache": true 26 | } 27 | }, 28 | "namedInputs": { 29 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 30 | "production": [ 31 | "default", 32 | "!{projectRoot}/.eslintrc.json", 33 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 34 | "!{projectRoot}/tsconfig.spec.json" 35 | ], 36 | "sharedGlobals": [] 37 | }, 38 | "workspaceLayout": { 39 | "appsDir": "packages", 40 | "libsDir": "packages" 41 | }, 42 | "nxCloudAccessToken": "NmYxNjExYTQtNGQxOS00MjBlLWI0ODAtZTJiZDZhYjhmZGE5fHJlYWQtd3JpdGU=" 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qwik-angular/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "prepare": "husky install" 7 | }, 8 | "private": true, 9 | "packageManager": "pnpm@8.10.5", 10 | "engines": { 11 | "pnpm": "^8.0.0" 12 | }, 13 | "devDependencies": { 14 | "@builder.io/qwik": "~1.2.6", 15 | "@builder.io/qwik-city": "~1.2.6", 16 | "@nx/angular": "17.1.1", 17 | "@nx/devkit": "17.1.1", 18 | "@nx/eslint-plugin": "17.1.1", 19 | "@nx/js": "17.1.1", 20 | "@nx/vite": "17.1.1", 21 | "@nx/workspace": "17.1.1", 22 | "@swc/cli": "~0.1.62", 23 | "@swc/core": "1.3.96", 24 | "@types/node": "^18.16.1", 25 | "@types/yargs": "^17.0.24", 26 | "@typescript-eslint/eslint-plugin": "6.10.0", 27 | "@typescript-eslint/parser": "6.10.0", 28 | "@vitest/ui": "~0.32.0", 29 | "chalk": "^4.1.2", 30 | "eslint": "8.46.0", 31 | "eslint-config-prettier": "9.0.0", 32 | "eslint-plugin-qwik": "~1.2.6", 33 | "node-fetch": "~3.3.0", 34 | "nx": "17.1.1", 35 | "prettier": "^2.6.2", 36 | "qwik-nx": "^2.0.2", 37 | "rollup": "^3.27.1", 38 | "tslib": "^2.3.0", 39 | "typescript": "5.2.2", 40 | "undici": "^5.22.0", 41 | "verdaccio": "^5.0.4", 42 | "vite": "~4.4.0", 43 | "vite-plugin-dts": "~2.3.0", 44 | "vite-tsconfig-paths": "~4.2.0", 45 | "vitest": "~0.32.0", 46 | "yargs": "^17.7.2", 47 | "@nx/eslint": "17.1.1", 48 | "husky": "^8.0.0", 49 | "@commitlint/cli": "^17.6.5", 50 | "@commitlint/config-conventional": "^17.6.5", 51 | "lint-staged": "^13.2.2" 52 | }, 53 | "nx": { 54 | "includedScripts": [] 55 | }, 56 | "dependencies": { 57 | "@swc/helpers": "0.5.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QwikDev/qwik-angular/5b61b96eb2abeb807eaff1ef6781e013561c5af2/packages/.gitkeep -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "rules": {}, 4 | "ignorePatterns": ["!**/*"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": {} 9 | }, 10 | { 11 | "files": ["*.ts", "*.tsx"], 12 | "rules": {} 13 | }, 14 | { 15 | "files": ["*.js", "*.jsx"], 16 | "rules": {} 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/README.md: -------------------------------------------------------------------------------- 1 | # add-angular-to-qwik 2 | 3 | ## What is It? 4 | 5 | This is a simple utility package that runs an integration to add Angular to your existing Qwik application. 6 | 7 | Run `npx add-angular-to-qwik@latest` to try it out! 8 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/bin/add-angular-to-qwik.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import * as yargs from 'yargs'; 3 | import * as chalk from 'chalk'; 4 | 5 | import { CreateWorkspaceOptions } from 'create-nx-workspace/'; 6 | import { 7 | getPackageManagerCommand, 8 | detectPackageManager, 9 | } from 'nx/src/utils/package-manager'; 10 | import { output } from 'create-nx-workspace/src/utils/output'; 11 | import { readFileSync, writeFileSync, rmSync } from 'fs'; 12 | import { execSync } from 'child_process'; 13 | 14 | interface Arguments extends CreateWorkspaceOptions { 15 | installMaterialExample: boolean; 16 | } 17 | 18 | export const commandsObject: yargs.Argv = yargs 19 | .wrap(yargs.terminalWidth()) 20 | .parserConfiguration({ 21 | 'strip-dashed': true, 22 | 'dot-notation': true, 23 | }) 24 | .command( 25 | // this is the default and only command 26 | '$0 [options]', 27 | 'Add Angular to the Qwik workspace', 28 | (yargs) => [ 29 | yargs.option('installMaterialExample', { 30 | describe: chalk.dim`Add dependencies for the Angular Material and qwikified example component, that uses it`, 31 | type: 'boolean', 32 | }), 33 | ], 34 | 35 | async (argv: yargs.ArgumentsCamelCase) => { 36 | await main(argv).catch((error) => { 37 | const { version } = require('../package.json'); 38 | output.error({ 39 | title: `Something went wrong! v${version}`, 40 | }); 41 | throw error; 42 | }); 43 | } 44 | ) 45 | .help('help', chalk.dim`Show help`) 46 | .version( 47 | 'version', 48 | chalk.dim`Show version`, 49 | require('../package.json').version 50 | ) as yargs.Argv; 51 | 52 | async function main(parsedArgs: yargs.Arguments) { 53 | let isQwikNxInstalled = false; 54 | const pm = getRelevantPackageManagerCommand(); 55 | 56 | output.log({ 57 | title: `Adding Angular to your workspace.`, 58 | bodyLines: [ 59 | 'To make sure the command works reliably in all environments, and that the integration is applied correctly,', 60 | `We will run "${pm.install}" several times. Please wait.`, 61 | ], 62 | }); 63 | 64 | try { 65 | // letting Nx think that's an Nx repo 66 | writeFileSync( 67 | 'project.json', 68 | JSON.stringify({ 69 | name: 'temp-project', 70 | sourceRoot: 'src', 71 | projectType: 'application', 72 | targets: {}, 73 | }) 74 | ); 75 | 76 | isQwikNxInstalled = checkIfPackageInstalled('qwik-nx'); 77 | 78 | if (!isQwikNxInstalled) { 79 | execSync(`${pm.add} qwik-nx@latest nx@latest`, { stdio: [0, 1, 2] }); 80 | } 81 | const installMaterialExample = parsedArgs['installMaterialExample']; 82 | const installMaterialExampleFlag = 83 | installMaterialExample === true || installMaterialExample === false 84 | ? `--installMaterialExample=${parsedArgs['installMaterialExample']}` 85 | : undefined; 86 | 87 | const cmd = [ 88 | 'npx nx g qwik-nx:angular-in-app', 89 | '--project=temp-project', 90 | installMaterialExampleFlag, 91 | '--skipFormat', 92 | ].filter(Boolean); 93 | execSync(cmd.join(' '), { stdio: [0, 1, 2] }); 94 | } catch (error) { 95 | output.error({ 96 | title: 'Failed to add angular to your repo', 97 | bodyLines: ['Reverting changes.', 'See original printed error above.'], 98 | }); 99 | cleanup(isQwikNxInstalled, pm.uninstall); 100 | process.exit(1); 101 | } 102 | 103 | cleanup(isQwikNxInstalled, pm.uninstall); 104 | 105 | output.log({ 106 | title: `Successfully added Angular integration to your repo`, 107 | }); 108 | } 109 | 110 | function checkIfPackageInstalled(pkg: string): boolean { 111 | const packageJson = JSON.parse(readFileSync('package.json', 'utf-8')); 112 | return ( 113 | !!packageJson['dependencies']?.[pkg] || 114 | !!packageJson['devDependencies']?.[pkg] 115 | ); 116 | } 117 | 118 | function getRelevantPackageManagerCommand() { 119 | const pm = detectPackageManager(); 120 | const pmc = getPackageManagerCommand(pm); 121 | let uninstall: string; 122 | if (pm === 'npm') { 123 | uninstall = 'npm uninstall'; 124 | } else if (pm === 'yarn') { 125 | uninstall = 'yarn remove'; 126 | } else { 127 | uninstall = 'pnpm remove'; 128 | } 129 | 130 | return { 131 | install: pmc.install, 132 | add: pmc.add, 133 | uninstall, 134 | }; 135 | } 136 | 137 | function cleanup(isQwikNxInstalled: boolean, uninstallCmd: string) { 138 | rmSync('.nx', { force: true, recursive: true }); 139 | rmSync('project.json'); 140 | if (!isQwikNxInstalled) { 141 | // TODO: remove deps from package.json and simply run npm install 142 | execSync(`${uninstallCmd} qwik-nx nx`, { stdio: [0, 1, 2] }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { commandsObject } from './add-angular-to-qwik'; 4 | 5 | commandsObject.argv; 6 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | transform: { 4 | '^.+\\.[tj]sx?$': 'ts-jest', 5 | }, 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 7 | globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, 8 | displayName: 'add-angular-to-qwik', 9 | testEnvironment: 'node', 10 | preset: '../../jest.preset.js', 11 | }; 12 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add-angular-to-qwik", 3 | "version": "0.2.0", 4 | "private": false, 5 | "description": "Adds Angular to the Qwik repo", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/QwikDev/qwik-angular.git", 9 | "directory": "packages/add-angular-to-qwik" 10 | }, 11 | "keywords": [ 12 | "Qwik", 13 | "Angular", 14 | "Web", 15 | "CLI" 16 | ], 17 | "bin": { 18 | "add-angular-to-qwik": "./bin/index.js" 19 | }, 20 | "author": "Dmitriy Stepanenko", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/QwikDev/qwik-angular/issues" 24 | }, 25 | "homepage": "https://github.com/QwikDev/qwik-angular", 26 | "peerDependencies": { 27 | "qwik-nx": "^2.0.2", 28 | "create-nx-workspace": "17.1.1" 29 | }, 30 | "publishConfig": { 31 | "access": "public", 32 | "provenance": false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add-angular-to-qwik", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/add-angular-to-qwik", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/add-angular-to-qwik", 12 | "main": "packages/add-angular-to-qwik/bin/add-angular-to-qwik.ts", 13 | "tsConfig": "packages/add-angular-to-qwik/tsconfig.lib.json", 14 | "assets": [ 15 | "packages/add-angular-to-qwik/README.md", 16 | { 17 | "input": "packages/add-angular-to-qwik", 18 | "glob": "**/files/**", 19 | "output": "/" 20 | }, 21 | { 22 | "input": "packages/add-angular-to-qwik", 23 | "glob": "**/files/**/.gitkeep", 24 | "output": "/" 25 | }, 26 | { 27 | "input": "packages/add-angular-to-qwik", 28 | "glob": "**/*.json", 29 | "ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"], 30 | "output": "/" 31 | }, 32 | { 33 | "input": "packages/add-angular-to-qwik", 34 | "glob": "**/*.js", 35 | "ignore": ["**/jest.config.js"], 36 | "output": "/" 37 | }, 38 | { 39 | "input": "packages/add-angular-to-qwik", 40 | "glob": "**/*.d.ts", 41 | "output": "/" 42 | }, 43 | { 44 | "input": "", 45 | "glob": "LICENSE", 46 | "output": "/" 47 | } 48 | ] 49 | } 50 | }, 51 | "lint": { 52 | "executor": "@nx/eslint:lint", 53 | "options": { 54 | "lintFilePatterns": [ 55 | "packages/add-angular-to-qwik/**/*.ts", 56 | "packages/add-angular-to-qwik/**/*.spec.ts", 57 | "packages/add-angular-to-qwik/**/*_spec.ts", 58 | "packages/add-angular-to-qwik/**/*.spec.tsx", 59 | "packages/add-angular-to-qwik/**/*.spec.js", 60 | "packages/add-angular-to-qwik/**/*.spec.jsx", 61 | "packages/add-angular-to-qwik/**/*.d.ts" 62 | ] 63 | }, 64 | "outputs": ["{options.outputFile}"] 65 | }, 66 | "version": { 67 | "executor": "@jscutlery/semver:version", 68 | "options": {} 69 | }, 70 | "version-publish": { 71 | "executor": "@jscutlery/semver:version", 72 | "options": { 73 | "noVerify": true, 74 | "push": false, 75 | "releaseAs": "patch", 76 | "postTargets": [ 77 | "add-angular-to-qwik:publish", 78 | "add-angular-to-qwik:push-to-github" 79 | ] 80 | } 81 | }, 82 | "publish": { 83 | "executor": "ngx-deploy-npm:deploy", 84 | "options": { 85 | "access": "public" 86 | }, 87 | "configurations": { 88 | "local": { 89 | "registry": "http://localhost:4873" 90 | } 91 | } 92 | }, 93 | "push-to-github": { 94 | "executor": "@jscutlery/semver:github", 95 | "options": { 96 | "tag": "${tag}", 97 | "notes": "${notes}" 98 | } 99 | } 100 | }, 101 | "implicitDependencies": [] 102 | } 103 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "strict": true 6 | }, 7 | "include": [], 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.lib.json" 12 | }, 13 | { 14 | "path": "./tsconfig.spec.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": [ 10 | "**/*.spec.ts", 11 | "**/*.test.ts", 12 | "**/*_spec.ts", 13 | "**/*_test.ts", 14 | "jest.config.ts" 15 | ], 16 | "include": ["**/*.ts", "package.json", "bin/add-angular-to-qwik.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/add-angular-to-qwik/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*_spec.ts", 12 | "**/*_test.ts", 13 | "**/*.spec.tsx", 14 | "**/*.test.tsx", 15 | "**/*.spec.js", 16 | "**/*.test.js", 17 | "**/*.spec.jsx", 18 | "**/*.test.jsx", 19 | "**/*.d.ts", 20 | "jest.config.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/qwik-angular/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/qwik-angular/README.md: -------------------------------------------------------------------------------- 1 | # Qwik Angular 2 | 3 | QwikAngular allows you to use Angular components in Qwik, including the whole ecosystem of component libraries. 4 | 5 | ## Installation 6 | 7 | Inside your Qwik app run: 8 | 9 | ```shell 10 | npx add-angular-to-qwik@latest 11 | ``` 12 | 13 | If you don't have a Qwik app yet, then you need to [create one first](../../../docs/getting-started/index.mdx), then, follow the instructions and run the command add Angular to your app. 14 | 15 | ```shell 16 | npm create qwik@latest 17 | cd to-my-app 18 | npx add-angular-to-qwik@latest 19 | ``` 20 | 21 | Be aware that in the dev mode the app may have to reload for a few times while vite optimizes Angular dependencies. This will not be the case in the built app. 22 | 23 | ## Usage 24 | 25 | The @builder.io/qwik-angular package exports the qwikify$() function that lets you convert Angular components into Qwik components, that you can use across your application. 26 | 27 | Angular and Qwik components can not be mixed in the same file, if you check your project right after running the installation command, you will see a new folder src/integrations/angular/components, from now on, all your Angular components will live there. Qwikified components are declared and exported from src/integrations/angular/index.ts file, it is important to not place Qwik code in the Angular component files. 28 | 29 | ## Limitations 30 | 31 | ## Defining A Component 32 | 33 | The Qwik Angular integration **only** supports rendering standalone components. You can still compose required UIs with Angular code that uses modules by wrapping it inside standalone components: 34 | 35 | ```ts 36 | import { NgIf } from '@angular/common'; 37 | import { Component, Input } from '@angular/core'; 38 | import { MatButtonModule } from '@angular/material/button'; 39 | 40 | @Component({ 41 | selector: 'app-hello', 42 | standalone: true, 43 | imports: [NgIf, MatButtonModule], 44 | template: ` 45 |

Hello from Angular!!

46 | 47 |

{{ helpText }}

48 | 49 | 50 | `, 51 | }) 52 | export class HelloComponent { 53 | @Input() helpText = 'help'; 54 | 55 | show = false; 56 | 57 | toggle() { 58 | this.show = !this.show; 59 | } 60 | } 61 | ``` 62 | 63 | ### Adding types 64 | 65 | Angular defines inputs and outputs of the component using decorators, so it's not possible to deduce them as interface properties for the qwikified component to be used as JSX element. This package provides utility types `QwikifiedComponentProps` and `WithRequiredProps` to simplify the creation of typed qwikified components. 66 | 67 | `QwikifiedComponentProps` utility expects the list of keys that represent inputs and outputs of your component. It does the following: 68 | 69 | - validates that provided keys exist within the component 70 | - extracts types of the provided keys for inputs 71 | - converts outputs to the handlers that can be used with JSX syntax (e.g. `EventEmitter` is converted to `(value: string) => void`) 72 | - adds `$` suffix for outputs, which basically [lets Qwik treat them as QRLs](https://qwik.builder.io/docs/advanced/dollar) 73 | 74 | Here's an example of how it works: 75 | 76 | ```ts 77 | @Component({..}) 78 | export class InputComponent { 79 | @Input() theme: 'primary' | 'accent' | 'warn' = 'primary'; 80 | @Input() placeholder: string; 81 | @Output() changed = new EventEmitter(); 82 | } 83 | 84 | type InputComponentInputs = 'theme' | 'placeholder'; 85 | 86 | type InputComponentOutputs = 'changed'; 87 | 88 | // InputComponentProps is the interface that you can export along with your qwikified component to be used elsewhere 89 | export type InputComponentProps = QwikifiedComponentProps< 90 | InputComponent, 91 | InputComponentInputs, // inputs of the "InputComponent" 92 | InputComponentProps // outputs of the "InputComponent" 93 | >; 94 | 95 | // The final type will look like 96 | interface FinalInputTypeSample { 97 | theme?: 'primary' | 'accent' | 'warn'; 98 | placeholder?: string; 99 | changed$?: (value: string) => void; // notice that "changed" output got a "$" suffix! 100 | } 101 | // qwikify it later as follows 102 | export const MyNgInput = qwikify$(InputComponent); 103 | 104 | // additionally you can mark types as required using "WithRequiredProps" util 105 | type RequiredInputProps = 'theme'; 106 | export type RequiredInputComponentProps = WithRequiredProps; 107 | 108 | // The assembled type will have "theme" as a required property this time 109 | interface FinalInputTypeRequiredSample { 110 | theme: 'primary' | 'accent' | 'warn'; // <= became required! 111 | placeholder?: string; 112 | changed$?: (value: string) => void; 113 | } 114 | ``` 115 | 116 | ### Every qwikified Angular component is isolated 117 | 118 | Each instance of a qwikified Angular component becomes an independent Angular app. Fully isolated. 119 | 120 | ```tsx 121 | export const AngularHelloComponent = qwikify$(HelloComponent); 122 | ; 123 | ``` 124 | 125 | - Each `AngularHelloComponent` is a fully isolated Angular application, with its own state, lifecycle, etc. 126 | - Styles will be duplicated 127 | - State will not be shared. 128 | - Islands will hydrate independently 129 | 130 | ### Use `qwikify$()` as a migration strategy 131 | 132 | Using Angular components in Qwik is a great way to migrate your application to Qwik, but it's not a silver bullet, you will need to rewrite your components to take advantage of Qwik's features. 133 | 134 | It's also a great way to enjoy the Angular ecosystem. 135 | 136 | > Don't abuse of `qwikify$()` to build your own application, all performance gains will be lost. 137 | 138 | ### Build wide islands, not leaf nodes 139 | 140 | For example, if you need to use several Angular components, to build a list, don't qwikify each individual component, instead, build the whole list as a single qwikified Angular component. 141 | 142 | #### GOOD: Wide island 143 | 144 | A single qwikified component, with all the Angular components inside. Styles will not be duplicated, and context and theming will work as expected. 145 | 146 | ```ts 147 | // folder.component.ts 148 | import List from './list.component'; 149 | import ListItem from './list-item.component'; 150 | import ListItemText from './list-item-text.component'; 151 | import ListItemAvatar from './list-item-avatar.component'; 152 | import Avatar from './avatar.component'; 153 | import Icon from './icon.component'; 154 | 155 | // Qwikify the whole list 156 | @Component({ 157 | standalone: true, 158 | imports: [List, ListItem, ListItemText, ListItemAvatar, Avatar, Icon], 159 | template: ` 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | `, 187 | }) 188 | export class FolderList {} 189 | ``` 190 | 191 | #### BAD: Leaf nodes 192 | 193 | Leaf nodes are qwikified independently, effectively rendering dozens of nested Angular applications, each fully isolated from the others, and styles being duplicated. 194 | 195 | ```tsx 196 | import List from './list.component'; 197 | import ListItem from './list-item.component'; 198 | import ListItemText from './list-item-text.component'; 199 | import ListItemAvatar from './list-item-avatar.component'; 200 | import Avatar from './avatar.component'; 201 | import Icon from './icon.component'; 202 | 203 | export const AngularList = qwikify$(List); 204 | export const AngularListItem = qwikify$(ListItem); 205 | export const AngularListItemText = qwikify$(ListItemText); 206 | export const AngularListItemAvatar = qwikify$(ListItemAvatar); 207 | export const AngularAvatar = qwikify$(Avatar); 208 | export const AngularIcon = qwikify$(Icon); 209 | ``` 210 | 211 | ```tsx 212 | // Qwik component using dozens of nested Angular islands 213 | // Each Angular-* is an independent Angular application 214 | export const FolderList = component$(() { 215 | return ( 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | ); 243 | }); 244 | ``` 245 | 246 | ## Adding interactivity 247 | 248 | In order to add interactivity it is required to hydrate the Angular application. Angular uses [destructive hydration](https://blog.angular.io/angulars-vision-for-the-future-3cfca5e7b448), which means components are rendered on the server and then are fully recreated on the client. This [adds a massive overhead](https://www.builder.io/blog/hydration-is-pure-overhead) and making sites slow. 249 | 250 | Qwik allows you decide when to hydrate your components, by using the `client:` JSX properties, this technique is commonly called partial hydration, popularized by [Astro](https://astro.build/). 251 | 252 | ```diff 253 | export default component$(() => { 254 | return ( 255 | <> 256 | - 257 | + 258 | 259 | ); 260 | }); 261 | ``` 262 | 263 | Qwik comes with different strategies out of the box: 264 | 265 | ### `client:load` 266 | 267 | The component eagerly hydrates when the document loads. 268 | 269 | ```tsx 270 | 271 | ``` 272 | 273 | **Use case:** Immediately-visible UI elements that need to be interactive as soon as possible. 274 | 275 | ### `client:idle` 276 | 277 | The component eagerly hydrates when the browser first become idle, ie, when everything important as already run before. 278 | 279 | ```tsx 280 | 281 | ``` 282 | 283 | **Use case:** Lower-priority UI elements that don’t need to be immediately interactive. 284 | 285 | ### `client:visible` 286 | 287 | The component eagerly hydrates when it becomes visible in the viewport. 288 | 289 | ```tsx 290 | 291 | ``` 292 | 293 | **Use case:** Low-priority UI elements that are either far down the page (“below the fold”) or so resource-intensive to load that you would prefer not to load them at all if the user never saw the element. 294 | 295 | ### `client:hover` 296 | 297 | The component eagerly hydrates when the mouse is over the component. 298 | 299 | ```tsx 300 | 301 | ``` 302 | 303 | **Use case:** Lowest-priority UI elements which interactivity is not crucial, and only needs to run in desktop. 304 | 305 | ### `client:signal` 306 | 307 | This is an advanced API that allows to hydrate the component whenever the passed signal becomes `true`. 308 | 309 | ```tsx 310 | export default component$(() => { 311 | const hydrateAngular = useSignal(false); 312 | return ( 313 | <> 314 | 317 | 318 | 319 | ); 320 | }); 321 | ``` 322 | 323 | This effectively allows you to implement custom strategies for hydration. 324 | 325 | ### `client:event` 326 | 327 | The component eagerly hydrates when specified DOM events are dispatched. 328 | 329 | ```tsx 330 | 331 | ``` 332 | 333 | ### `client:only` 334 | 335 | When `true`, the component will not run in SSR, only in the browser. 336 | 337 | ```tsx 338 | 339 | ``` 340 | 341 | ## Listening to Angular events 342 | 343 | Events in Angular are propagated as component outputs: 344 | 345 | ```html 346 | 347 | ; 348 | ``` 349 | 350 | The `qwikify()` function will convert all outputs into properties with functional handlers 351 | 352 | ```tsx 353 | import { Slider } from './components'; 354 | import { qwikify$ } from '@builder.io/qwik-angular'; 355 | const AngularSlider = qwikify$(Slider); 356 | console.log('value changed')} />; 357 | ``` 358 | 359 | > Notice that we use the `client:visible` property to eagerly hydrate the component, otherwise the component would not be interactive and the events would never be dispatched. 360 | 361 | ## Host element 362 | 363 | When wrapping an Angular component with `qwikify$()`, under the hood, a new DOM element is created, such as: 364 | 365 | ```html 366 | 367 | 368 | 369 | ``` 370 | 371 | > Notice, that the tag name of the wrapper element is configurable via `tagName`: `qwikify$(AngularCmp, { tagName: 'my-ng' })`. 372 | 373 | ### Listen to DOM events without hydration 374 | 375 | The host element is not part of Angular, meaning that hydration is not necessary to listen for events, in order to add custom attributes and events to the host element, you can use the `host:` prefix in the JSX properties, such as: 376 | 377 | ```tsx 378 | { 380 | console.log('click an Angular component without hydration!!'); 381 | }} 382 | /> 383 | ``` 384 | 385 | This will effectively allow you to respond to a click in an Angular button without downloading a single byte of Angular code. 386 | 387 | Happy hacking! 388 | -------------------------------------------------------------------------------- /packages/qwik-angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qwikdev/qwik-angular", 3 | "version": "0.2.0", 4 | "description": "QwikAngular allows adding Angular components into existing Qwik application", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/QwikDev/qwik-angular.git", 8 | "directory": "packages/qwik-angular" 9 | }, 10 | "keywords": [ 11 | "Qwik", 12 | "Angular", 13 | "Web", 14 | "CLI" 15 | ], 16 | "author": "Dmitriy Stepanenko", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/QwikDev/qwik-angular/issues" 20 | }, 21 | "homepage": "https://github.com/QwikDev/qwik-angular", 22 | "publishConfig": { 23 | "access": "public", 24 | "provenance": false 25 | }, 26 | "main": "./index.qwik.mjs", 27 | "qwik": "./index.qwik.mjs", 28 | "types": "./index.qwik.d.ts", 29 | "dependencies": { 30 | "@analogjs/vite-plugin-angular": "~0.2.0", 31 | "@angular-devkit/build-angular": "^17.1.0", 32 | "@angular/animations": "^17.1.0", 33 | "@angular/common": "^17.1.0", 34 | "@angular/compiler-cli": "^17.1.0", 35 | "@angular/compiler": "^17.1.0", 36 | "@angular/core": "^17.1.0", 37 | "@angular/platform-browser-dynamic": "^17.1.0", 38 | "@angular/platform-browser": "^17.1.0", 39 | "@angular/platform-server": "^17.1.0", 40 | "@builder.io/qwik": "^1.2.11", 41 | "rxjs": "~7.8.0", 42 | "sass": "^1.60.0", 43 | "zone.js": "~0.14.2" 44 | }, 45 | "exports": { 46 | ".": { 47 | "import": "./index.qwik.mjs", 48 | "require": "./index.qwik.cjs", 49 | "types": "./index.qwik.d.ts" 50 | }, 51 | "./vite": { 52 | "import": "./vite.mjs", 53 | "require": "./vite.cjs", 54 | "types": "./vite.d.ts" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/qwik-angular/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qwik-angular", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/qwik-angular/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/vite:build", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/qwik-angular", 12 | "mode": "lib" 13 | } 14 | }, 15 | "publish": { 16 | "command": "node tools/scripts/publish.mjs qwik-angular {args.ver} {args.tag}", 17 | "dependsOn": ["build"] 18 | }, 19 | "lint": { 20 | "executor": "@nx/eslint:lint", 21 | "outputs": ["{options.outputFile}"], 22 | "options": { 23 | "lintFilePatterns": ["packages/qwik-angular/**/*.{ts,tsx,js,jsx}"] 24 | } 25 | } 26 | }, 27 | "tags": [] 28 | } 29 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/index.qwik.ts: -------------------------------------------------------------------------------- 1 | export { qwikify$, qwikifyQrl } from './lib/qwikify'; 2 | 3 | export type { 4 | QwikifyProps, 5 | QwikifiedComponentProps, 6 | WithRequiredProps, 7 | } from './lib/types'; 8 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './index.qwik'; 2 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createComponent, 3 | type EventEmitter, 4 | NgZone, 5 | reflectComponentType, 6 | type ApplicationRef, 7 | type ComponentRef, 8 | type ComponentMirror, 9 | type Type, 10 | } from '@angular/core'; 11 | import { createApplication } from '@angular/platform-browser'; 12 | import { merge, Subject } from 'rxjs'; 13 | import { map, takeUntil } from 'rxjs/operators'; 14 | import { extractProjectableNodes } from './extract-projectable-nodes'; 15 | import zoneJs from 'zone.js/plugins/zone.min?url'; 16 | import { getAngularProps } from './slot'; 17 | import { provideAnimations } from '@angular/platform-browser/animations'; 18 | 19 | declare const Zone: unknown; 20 | 21 | export class ClientRenderer { 22 | appRef: ApplicationRef | undefined; 23 | 24 | private componentRef!: ComponentRef; 25 | 26 | private mirror: ComponentMirror | null = null; 27 | private readonly knownInputs = new Set(); 28 | private readonly knownOutputs = new Set(); 29 | 30 | private readonly outputHandlers = new Map void>(); 31 | 32 | private readonly onDestroy$ = new Subject(); 33 | 34 | private initialized = false; 35 | 36 | constructor( 37 | private component: Type, 38 | private initialProps: Record 39 | ) {} 40 | 41 | async render( 42 | hostElement: Element, 43 | slot: Element | undefined, 44 | props = this.initialProps 45 | ) { 46 | await this.loadZoneJs(); 47 | try { 48 | this.appRef = await createApplication({ 49 | providers: [provideAnimations()], 50 | }); 51 | } catch (error) { 52 | console.error('Failed to qwikify Angular component', error); 53 | return; 54 | } 55 | const zone = this.appRef.injector.get(NgZone); 56 | 57 | zone.run(() => { 58 | this.mirror = reflectComponentType(this.component); 59 | 60 | const projectableNodes = 61 | slot && 62 | extractProjectableNodes(slot, [ 63 | ...(this.mirror?.ngContentSelectors ?? []), 64 | ]); 65 | 66 | this.componentRef = createComponent(this.component, { 67 | environmentInjector: this.appRef!.injector, 68 | hostElement: hostElement, 69 | projectableNodes, 70 | }); 71 | 72 | this.componentRef.onDestroy(() => this.onDestroy$.next()); 73 | 74 | this.mirror?.inputs.forEach((i) => { 75 | this.knownInputs.add(i.propName); 76 | this.knownInputs.add(i.templateName); 77 | }); 78 | this.mirror?.outputs.forEach((i) => { 79 | this.knownOutputs.add(i.templateName); 80 | }); 81 | 82 | this.setInputProps(props, true); 83 | 84 | this._subscribeToEvents(); 85 | 86 | this.appRef!.attachView(this.componentRef.hostView); 87 | this.initialized = true; 88 | }); 89 | } 90 | 91 | setInputProps(props: Record, skipInitCheck = false) { 92 | if (!this.initialized && !skipInitCheck) { 93 | return; 94 | } 95 | 96 | // propsEntries will include output props w\o "$" suffix 97 | const propsEntries = Object.entries(getAngularProps(props)); 98 | 99 | for (const [key, value] of propsEntries) { 100 | if (this.knownInputs.has(key)) { 101 | this.componentRef.setInput(key, value); 102 | } 103 | if (this.knownOutputs.has(key)) { 104 | if (typeof value === 'function') { 105 | this.outputHandlers.set(key, value); 106 | } else { 107 | console.warn( 108 | `"${key}" param expects a callback function, got "${typeof value}" instead.` 109 | ); 110 | } 111 | } 112 | } 113 | 114 | this.appRef!.tick(); 115 | } 116 | 117 | private _subscribeToEvents(): void { 118 | if (!this.mirror) { 119 | return; 120 | } 121 | 122 | const eventEmitters = this.mirror.outputs.map( 123 | ({ propName, templateName }) => { 124 | const emitter = (this.componentRef.instance as any)[ 125 | propName 126 | ] as EventEmitter; 127 | return emitter.pipe( 128 | map((value: any) => ({ name: templateName, value })) 129 | ); 130 | } 131 | ); 132 | const outputEvents = merge(...eventEmitters); 133 | // listen for events from the merged stream and dispatch them as custom events 134 | outputEvents.pipe(takeUntil(this.onDestroy$)).subscribe((e) => { 135 | // emit the event to the registered handler 136 | this.outputHandlers.get(e.name)?.(e.value); 137 | }); 138 | } 139 | 140 | private async loadZoneJs(): Promise { 141 | return new Promise((resolve, reject) => { 142 | if (typeof Zone === 'function') { 143 | return resolve(); 144 | } 145 | const script = document.createElement('script'); 146 | script.src = zoneJs; 147 | script.onload = () => resolve(); 148 | script.onerror = reject; 149 | document.head.appendChild(script); 150 | script.remove(); 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/extract-projectable-nodes.ts: -------------------------------------------------------------------------------- 1 | // source: https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/elements/src/extract-projectable-nodes.ts 2 | // this util is not exposed from the Angular package, thus copying it here 3 | 4 | export function extractProjectableNodes( 5 | host: Element, 6 | ngContentSelectors: string[] 7 | ): Node[][] { 8 | const nodes = host.childNodes; 9 | const projectableNodes: Node[][] = ngContentSelectors.map(() => []); 10 | let wildcardIndex = -1; 11 | 12 | ngContentSelectors.some((selector, i) => { 13 | if (selector === '*') { 14 | wildcardIndex = i; 15 | return true; 16 | } 17 | return false; 18 | }); 19 | 20 | for (let i = 0, ii = nodes.length; i < ii; ++i) { 21 | const node = nodes[i]; 22 | const ngContentIndex = findMatchingIndex( 23 | node, 24 | ngContentSelectors, 25 | wildcardIndex 26 | ); 27 | 28 | if (ngContentIndex !== -1) { 29 | projectableNodes[ngContentIndex].push(node); 30 | } 31 | } 32 | 33 | return projectableNodes; 34 | } 35 | 36 | function findMatchingIndex( 37 | node: Node, 38 | selectors: string[], 39 | defaultIndex: number 40 | ): number { 41 | let matchingIndex = defaultIndex; 42 | 43 | if (isElement(node)) { 44 | selectors.some((selector, i) => { 45 | if (selector !== '*' && matchesSelector(node, selector)) { 46 | matchingIndex = i; 47 | return true; 48 | } 49 | return false; 50 | }); 51 | } 52 | 53 | return matchingIndex; 54 | } 55 | 56 | // UTILS 57 | 58 | let _matches: (this: any, selector: string) => boolean; 59 | 60 | /** 61 | * Check whether an `Element` matches a CSS selector. 62 | * NOTE: this is duplicated from @angular/upgrade, and can 63 | * be consolidated in the future 64 | */ 65 | function matchesSelector(el: any, selector: string): boolean { 66 | if (!_matches) { 67 | const elProto = Element.prototype; 68 | _matches = 69 | elProto.matches || 70 | elProto.matchesSelector || 71 | elProto.mozMatchesSelector || 72 | elProto.msMatchesSelector || 73 | elProto.oMatchesSelector || 74 | elProto.webkitMatchesSelector; 75 | } 76 | return el.nodeType === Node.ELEMENT_NODE 77 | ? _matches.call(el, selector) 78 | : false; 79 | } 80 | 81 | /** 82 | * Check whether the input is an `Element`. 83 | */ 84 | export function isElement(node: Node | null): node is Element { 85 | return !!node && node.nodeType === Node.ELEMENT_NODE; 86 | } 87 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/qwikify.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | component$, 3 | implicit$FirstArg, 4 | RenderOnce, 5 | Slot, 6 | type NoSerialize, 7 | type QRL, 8 | useSignal, 9 | useTask$, 10 | noSerialize, 11 | useVisibleTask$, 12 | useStylesScoped$, 13 | } from '@builder.io/qwik'; 14 | import { isBrowser, isServer } from '@builder.io/qwik/build'; 15 | import type { Type } from '@angular/core'; 16 | import { ClientRenderer } from './client'; 17 | import { getHostProps } from './slot'; 18 | import type { Internal, QwikifyOptions, QwikifyProps } from './types'; 19 | import { renderFromServer } from './server'; 20 | import { useWakeupSignal } from './wake-up-signal'; 21 | 22 | export function qwikifyQrl>( 23 | angularCmp$: QRL>, 24 | qwikifyOptions?: QwikifyOptions 25 | ) { 26 | // TODO: check if provided angularCmp$ is a standalone angular component 27 | return component$>((props) => { 28 | useStylesScoped$(`q-slot:not([projected]){display:none}`); 29 | const hostRef = useSignal(); 30 | const slotRef = useSignal(); 31 | const internalState = useSignal>(); 32 | const [signal, isClientOnly] = useWakeupSignal(props, qwikifyOptions); 33 | const TagName: any = qwikifyOptions?.tagName ?? 'qwik-angular'; 34 | 35 | useVisibleTask$(({ cleanup }) => { 36 | cleanup(() => internalState.value?.renderer.appRef?.destroy()); 37 | }); 38 | 39 | // Watch takes care of updates and partial hydration 40 | useTask$(async ({ track }) => { 41 | const trackedProps = track>(() => ({ ...props })); 42 | track(signal); 43 | if (!isBrowser) { 44 | return; 45 | } 46 | 47 | // Update 48 | if (internalState.value) { 49 | if (internalState.value.renderer) { 50 | internalState.value.renderer.setInputProps(trackedProps); 51 | } 52 | } else { 53 | const component = await angularCmp$.resolve(); 54 | const hostElement = hostRef.value; 55 | const renderer = new ClientRenderer(component, trackedProps); 56 | if (hostElement) { 57 | await renderer.render(hostElement, slotRef.value); 58 | } 59 | internalState.value = noSerialize({ 60 | renderer, 61 | }); 62 | } 63 | }); 64 | 65 | if (isServer && !isClientOnly) { 66 | const jsx = renderFromServer( 67 | TagName, 68 | angularCmp$, 69 | hostRef, 70 | slotRef, 71 | props as Record 72 | ); 73 | return {jsx}; 74 | } 75 | 76 | return ( 77 | 78 | { 81 | queueMicrotask(async () => { 82 | // queueMicrotask is needed in order to have "slotRef" defined 83 | hostRef.value = el; 84 | if (isBrowser && internalState.value) { 85 | internalState.value.renderer && 86 | (await internalState.value.renderer.render( 87 | el, 88 | slotRef.value, 89 | props 90 | )); 91 | } 92 | }); 93 | }} 94 | > 95 | 96 | 97 | 98 | 99 | ); 100 | }); 101 | } 102 | 103 | export const qwikify$ = /*#__PURE__*/ implicit$FirstArg(qwikifyQrl); 104 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/server.tsx: -------------------------------------------------------------------------------- 1 | import { type QRL, type Signal, SSRRaw, Slot } from '@builder.io/qwik'; 2 | import { isServer } from '@builder.io/qwik/build'; 3 | import { 4 | reflectComponentType, 5 | ApplicationRef, 6 | type ComponentMirror, 7 | InjectionToken, 8 | type Provider, 9 | ɵRender3ComponentFactory, 10 | type Injector, 11 | type Type, 12 | NgZone, 13 | ɵNoopNgZone, 14 | } from '@angular/core'; 15 | import { 16 | BEFORE_APP_SERIALIZED, 17 | renderApplication, 18 | provideServerRendering, 19 | } from '@angular/platform-server'; 20 | import { bootstrapApplication } from '@angular/platform-browser'; 21 | import { DOCUMENT } from '@angular/common'; 22 | import { getHostProps } from './slot'; 23 | import { BehaviorSubject } from 'rxjs'; 24 | 25 | const SLOT_MARK = 'SLOT'; 26 | const SLOT_COMMENT = ``; 27 | 28 | const projectableNodesMap = new Set>(); 29 | const create = ɵRender3ComponentFactory.prototype.create; 30 | ɵRender3ComponentFactory.prototype.create = function ( 31 | injector: Injector, 32 | projectableNodes?: any[][], 33 | rootSelectorOrNode?: any, 34 | environmentInjector?: any 35 | ) { 36 | if (projectableNodesMap.has(this.componentType)) { 37 | const document_local: typeof document = 38 | this['ngModule'].injector.get(DOCUMENT); 39 | const slotComment = document_local.createComment(SLOT_MARK); 40 | projectableNodes = [[slotComment]]; // TODO: support multiple ng-content 41 | } 42 | return create.call( 43 | this, 44 | injector, 45 | projectableNodes, 46 | rootSelectorOrNode, 47 | environmentInjector 48 | ); 49 | }; 50 | 51 | const QWIK_ANGULAR_STATIC_PROPS = new InjectionToken<{ 52 | props: Record; 53 | mirror: ComponentMirror; 54 | }>('@builder.io/qwik-angular: Static Props w/ Mirror Provider', { 55 | factory() { 56 | return { props: {}, mirror: {} as ComponentMirror }; 57 | }, 58 | }); 59 | 60 | // Run beforeAppInitialized hook to set Input on the ComponentRef 61 | // before the platform renders to string 62 | const STATIC_PROPS_HOOK_PROVIDER: Provider = { 63 | provide: BEFORE_APP_SERIALIZED, 64 | useFactory: ( 65 | appRef: ApplicationRef, 66 | { 67 | props, 68 | mirror, 69 | }: { 70 | props: Record; 71 | mirror: ComponentMirror; 72 | } 73 | ) => { 74 | return () => { 75 | const compRef = appRef.components[0]; 76 | if (compRef && props && mirror) { 77 | for (const [key, value] of Object.entries(props)) { 78 | if ( 79 | // we double-check inputs on ComponentMirror 80 | // because there might be additional props 81 | // that aren't actually Input defined on the Component 82 | mirror.inputs.some( 83 | ({ templateName, propName }) => 84 | templateName === key || propName === key 85 | ) 86 | ) { 87 | compRef.setInput(key, value); 88 | } 89 | } 90 | compRef.changeDetectorRef.detectChanges(); 91 | } 92 | }; 93 | }, 94 | deps: [ApplicationRef, QWIK_ANGULAR_STATIC_PROPS], 95 | multi: true, 96 | }; 97 | 98 | class MockApplicationRef extends ApplicationRef { 99 | override isStable = new BehaviorSubject(true); 100 | } 101 | 102 | export async function renderFromServer( 103 | Host: any, 104 | angularCmp$: QRL>, 105 | hostRef: Signal, 106 | slotRef: Signal, 107 | props: Record 108 | ) { 109 | if (isServer) { 110 | const component = await angularCmp$.resolve(); 111 | const mirror = reflectComponentType(component); 112 | 113 | if (mirror?.ngContentSelectors.length) { 114 | projectableNodesMap.add(component); 115 | } 116 | 117 | const appId = mirror?.selector || component.name.toString().toLowerCase(); 118 | const document = `<${appId}>`; 119 | 120 | // There're certain issues with setting up zone.js in the qwik's node runtime 121 | // thus dropping it entirely for now 122 | // It might affect SSR in some sense, but should not be critical in most of the cases 123 | const mockZoneProviders = [ 124 | { 125 | provide: ApplicationRef, 126 | useFactory: () => new MockApplicationRef(), 127 | }, 128 | { provide: NgZone, useClass: ɵNoopNgZone }, 129 | ]; 130 | 131 | const bootstrap = () => 132 | bootstrapApplication(component, { 133 | providers: [ 134 | ...mockZoneProviders, 135 | { 136 | provide: QWIK_ANGULAR_STATIC_PROPS, 137 | useValue: { props, mirror }, 138 | }, 139 | STATIC_PROPS_HOOK_PROVIDER, 140 | provideServerRendering(), 141 | ], 142 | }); 143 | const html = await renderApplication(bootstrap, { 144 | document, 145 | }); 146 | const index = html.indexOf(SLOT_COMMENT); 147 | 148 | if (index > 0) { 149 | const part1 = html.slice(0, index); 150 | const part2 = html.slice(index + SLOT_COMMENT.length); 151 | return ( 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | ); 160 | } 161 | return ( 162 | <> 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | ); 171 | } 172 | return null; 173 | } 174 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/slot.ts: -------------------------------------------------------------------------------- 1 | export const getHostProps = ( 2 | props: Record 3 | ): Record => { 4 | const obj: Record = {}; 5 | Object.keys(props).forEach((key) => { 6 | if (key.startsWith(HOST_PREFIX)) { 7 | obj[key.slice(HOST_PREFIX.length)] = props[key]; 8 | } 9 | }); 10 | return obj; 11 | }; 12 | 13 | export const getAngularProps = ( 14 | props: Record 15 | ): Record => { 16 | const obj: Record = {}; 17 | Object.keys(props).forEach((key) => { 18 | if (!key.startsWith('client:') && !key.startsWith(HOST_PREFIX)) { 19 | const normalizedKey = key.endsWith('$') ? key.slice(0, -1) : key; 20 | obj[normalizedKey] = props[key]; 21 | } 22 | }); 23 | return obj; 24 | }; 25 | 26 | const HOST_PREFIX = 'host:'; 27 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from '@angular/core'; 2 | import type { PropFunction, Signal } from '@builder.io/qwik'; 3 | import type { ClientRenderer } from './client'; 4 | 5 | export interface Internal { 6 | renderer: ClientRenderer; 7 | } 8 | 9 | export interface QwikifyBase { 10 | /** 11 | * The component eagerly hydrates when the document loads. 12 | * 13 | * **Use case:** Immediately-visible UI elements that need to be interactive as soon as possible. 14 | */ 15 | 'client:load'?: boolean; 16 | 17 | /** 18 | * The component eagerly hydrates when the browser first become idle, 19 | * ie, when everything important as already run before. 20 | * 21 | * **Use case:** Lower-priority UI elements that don’t need to be immediately interactive. 22 | */ 23 | 'client:idle'?: boolean; 24 | 25 | /** 26 | * The component eagerly hydrates when it becomes visible in the viewport. 27 | * 28 | * **Use case:** Low-priority UI elements that are either far down the page 29 | * (“below the fold”) or so resource-intensive to load that 30 | * you would prefer not to load them at all if the user never saw the element. 31 | */ 32 | 'client:visible'?: boolean; 33 | 34 | /** 35 | * The component eagerly hydrates when the mouse is over the component. 36 | * 37 | * **Use case:** Lowest-priority UI elements which interactivity is not crucial, and only needs to run in desktop. 38 | */ 39 | 'client:hover'?: boolean; 40 | 41 | /** 42 | * When `true`, the component will not run in SSR, only in the browser. 43 | */ 44 | 'client:only'?: boolean; 45 | 46 | /** 47 | * This is an advanced API that allows to hydrate the component whenever 48 | * the passed signal becomes `true`. 49 | * 50 | * This effectively allows you to implement custom strategies for hydration. 51 | */ 52 | 'client:signal'?: Signal; 53 | 54 | /** 55 | * The component eagerly hydrates when specified DOM events are dispatched. 56 | */ 57 | 'client:event'?: string | string[]; 58 | 59 | /** 60 | * Adds a `click` event listener to the host element, this event will be dispatched even if the component is not hydrated. 61 | */ 62 | 'host:onClick$'?: PropFunction<(ev: Event) => void>; 63 | 64 | /** 65 | * Adds a `blur` event listener to the host element, this event will be dispatched even if the component is not hydrated. 66 | */ 67 | 'host:onBlur$'?: PropFunction<(ev: Event) => void>; 68 | 69 | /** 70 | * Adds a `focus` event listener to the host element, this event will be dispatched even if the component is not hydrated. 71 | */ 72 | 'host:onFocus$'?: PropFunction<(ev: Event) => void>; 73 | 74 | /** 75 | * Adds a `mouseover` event listener to the host element, this event will be dispatched even if the component is not hydrated. 76 | */ 77 | 'host:onMouseOver$'?: PropFunction<(ev: Event) => void>; 78 | } 79 | 80 | export type QwikifyProps> = PROPS & 81 | QwikifyBase; 82 | 83 | export interface QwikifyOptions { 84 | tagName?: string; 85 | eagerness?: 'load' | 'visible' | 'idle' | 'hover'; 86 | event?: string | string[]; 87 | clientOnly?: boolean; 88 | } 89 | 90 | type TransformKey = K extends string ? `${K}$` : K; 91 | 92 | type QwikifiedOutputs = { 93 | [K in keyof Pick< 94 | ComponentType, 95 | Props 96 | > as TransformKey]: ComponentType[K] extends EventEmitter 97 | ? (value: V) => void 98 | : never; 99 | }; 100 | 101 | // using "/@" instead of "@" in JSDoc because it's not rendering properly https://github.com/microsoft/TypeScript/issues/47679 102 | /** 103 | * Assembles a type object for qwikified Angular component 104 | * 105 | * @example 106 | * ``` 107 | * /@Component({..}) 108 | * export class InputComponent { 109 | * /@Input() theme: 'primary' | 'accent' | 'warn' = 'primary'; 110 | * /@Input() placeholder: string; 111 | * /@Output() changed = new EventEmitter(); 112 | * } 113 | * 114 | * type InputComponentInputs = 'theme' | 'placeholder'; 115 | * 116 | * type InputComponentOutputs = 'changed'; 117 | * 118 | * // InputComponentProps is the interface that you can export along with your qwikified component to be used elsewhere 119 | * export type InputComponentProps = QwikifiedComponentProps< 120 | * InputComponent, 121 | * InputComponentInputs, // inputs of the "InputComponent" 122 | * InputComponentProps // outputs of the "InputComponent" 123 | * >; 124 | * 125 | * // The final type will look like 126 | * interface FinalInputTypeSample { 127 | * theme?: 'primary' | 'accent' | 'warn'; 128 | * placeholder?: string; 129 | * changed$?: (value: string) => void; // notice that "changed" output got a "$" suffix! 130 | * } 131 | * // qwikify it later as follows 132 | * export const MyNgInput = qwikify$(InputComponent); 133 | * 134 | * // additionally you can mark types as required 135 | * type RequiredInputProps = 'theme'; 136 | * export type RequiredInputComponentProps = WithRequiredProps; 137 | * 138 | * // The assembled type will have "theme" as a required property this time 139 | * interface FinalInputTypeRequiredSample { 140 | * theme: 'primary' | 'accent' | 'warn'; // <= became required! 141 | * placeholder?: string; 142 | * changed$?: (value: string) => void; 143 | * } 144 | * ``` 145 | */ 146 | export type QwikifiedComponentProps< 147 | ComponentType, 148 | Inputs extends keyof ComponentType = never, 149 | Outputs extends keyof ComponentType = never 150 | > = Partial< 151 | Pick & QwikifiedOutputs 152 | >; 153 | 154 | /** 155 | * Marks provided keys `K` of type `T` as required 156 | * @example 157 | * ``` 158 | * interface MyOptionalType { 159 | * propOne?: string; 160 | * propTwo?: number 161 | * propThree?: string[] 162 | * } 163 | * type RequiredProps = 'propOne' | 'propThree'; 164 | * type MyRequiredType = WithRequiredProps; 165 | * 166 | * // final type will look like this 167 | * interface FinalInterface { 168 | * propOne: string; // <= became required 169 | * propTwo?: number; 170 | * propThree: string[]; // <= became required 171 | * } 172 | * ``` 173 | */ 174 | export type WithRequiredProps = Omit & 175 | Required>; 176 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/vite-plugin/vite.ts: -------------------------------------------------------------------------------- 1 | import originalAngularPluginsImport, { 2 | type PluginOptions as ViteAngularPluginOptions, 3 | } from '@analogjs/vite-plugin-angular'; 4 | import type { Plugin } from 'vite'; 5 | import { compile, type Options } from 'sass'; 6 | 7 | // default import from the '@analogjs/vite-plugin-angular' is not resolved properly 8 | const originalAngularPlugins = (originalAngularPluginsImport as any) 9 | .default as typeof originalAngularPluginsImport; 10 | 11 | const ANALOG_ANGULAR_PLUGIN = '@analogjs/vite-plugin-angular'; 12 | 13 | export type PluginOptions = ViteAngularPluginOptions & { 14 | componentsDir: string; 15 | bundleSassFilesInDevMode?: { 16 | paths: string[]; 17 | compileOptions?: Options<'sync'>; 18 | }; 19 | }; 20 | 21 | export function angular(options: PluginOptions) { 22 | const plugins = originalAngularPlugins(options); // returns an array of 2 plugins 23 | 24 | for (const p of plugins) { 25 | if (p.name === ANALOG_ANGULAR_PLUGIN) { 26 | // rename the transform method so that it is called manually 27 | const transform = p.transform; 28 | p.transform = function (code, id, ssrOpts) { 29 | if (!id.includes(options.componentsDir)) { 30 | return; 31 | } 32 | return (transform).call(this, code, id, ssrOpts); 33 | }; 34 | } 35 | } 36 | return [...plugins, analogQwikPlugin(options)]; 37 | } 38 | 39 | function analogQwikPlugin(options: PluginOptions) { 40 | const bundleSassFilePaths = options.bundleSassFilesInDevMode?.paths; 41 | let viteCommand: 'build' | 'serve' = 'serve'; 42 | 43 | const vitePluginQwikAngular: Plugin = { 44 | name: 'vite-plugin-qwik-angular', 45 | 46 | config(viteConfig, viteEnv) { 47 | viteCommand = viteEnv.command; 48 | 49 | return { 50 | optimizeDeps: { 51 | include: [ 52 | '@angular/core', 53 | '@angular/platform-browser', 54 | '@angular/platform-browser/animations', 55 | '@angular/compiler', 56 | '@angular/common', 57 | '@angular/common/http', 58 | '@angular/animations', 59 | '@angular/animations/browser', 60 | '@angular/platform-server', 61 | ], 62 | }, 63 | }; 64 | }, 65 | 66 | load: function (id) { 67 | if ( 68 | viteCommand === 'serve' && 69 | bundleSassFilePaths?.some((p) => id.includes(p)) 70 | ) { 71 | // TODO: normalize path 72 | id = id.replace(/\?(.*)/, ''); 73 | try { 74 | const compiledAsset = compile( 75 | id, 76 | options.bundleSassFilesInDevMode?.compileOptions 77 | ).css; 78 | return compiledAsset; 79 | } catch (e) { 80 | // if failed to compile, do nothing 81 | } 82 | } 83 | return; 84 | }, 85 | }; 86 | return vitePluginQwikAngular; 87 | } 88 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/lib/wake-up-signal.ts: -------------------------------------------------------------------------------- 1 | import { $, useOn, useOnDocument, useSignal } from '@builder.io/qwik'; 2 | import { isServer } from '@builder.io/qwik/build'; 3 | import type { QwikifyOptions, QwikifyProps } from './types'; 4 | 5 | export const useWakeupSignal = ( 6 | props: QwikifyProps>, 7 | opts: QwikifyOptions = {} 8 | ) => { 9 | const signal = useSignal(false); 10 | const activate = $(() => (signal.value = true)); 11 | const clientOnly = !!(props['client:only'] || opts?.clientOnly); 12 | if (isServer) { 13 | if (props['client:visible'] || opts?.eagerness === 'visible') { 14 | useOn('qvisible', activate); 15 | } 16 | if (props['client:idle'] || opts?.eagerness === 'idle') { 17 | useOnDocument('qidle', activate); 18 | } 19 | if (props['client:load'] || clientOnly || opts?.eagerness === 'load') { 20 | useOnDocument('qinit', activate); 21 | } 22 | if (props['client:hover'] || opts?.eagerness === 'hover') { 23 | useOn('mouseover', activate); 24 | } 25 | if (props['client:event']) { 26 | useOn(props['client:event'], activate); 27 | } 28 | if (opts?.event) { 29 | useOn(opts?.event, activate); 30 | } 31 | } 32 | return [signal, clientOnly, activate] as const; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/root.tsx: -------------------------------------------------------------------------------- 1 | // qwik compilation entry point 2 | export default {}; 3 | -------------------------------------------------------------------------------- /packages/qwik-angular/src/vite.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/vite-plugin/vite'; 2 | -------------------------------------------------------------------------------- /packages/qwik-angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "@builder.io/qwik", 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "lib": ["es2020", "DOM", "WebWorker", "DOM.Iterable"] 14 | }, 15 | "files": [], 16 | "include": [], 17 | "references": [ 18 | { 19 | "path": "./tsconfig.lib.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/qwik-angular/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node", "vite/client", "vitest"] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/qwik-angular/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Plugin, defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | import dts from 'vite-plugin-dts'; 6 | import * as path from 'path'; 7 | import { qwikVite } from '@builder.io/qwik/optimizer'; 8 | import type { OutputChunk } from 'rollup'; 9 | 10 | export default defineConfig({ 11 | cacheDir: '../../node_modules/.vite/qwik-angular', 12 | 13 | plugins: [ 14 | qwikVite(), 15 | dts({ 16 | entryRoot: 'src', 17 | tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), 18 | skipDiagnostics: true, 19 | }), 20 | 21 | nxViteTsPaths(), 22 | vitePluginFixBundle(), 23 | ], 24 | 25 | // Configuration for building your library. 26 | // See: https://vitejs.dev/guide/build.html#library-mode 27 | build: { 28 | minify: false, 29 | target: 'es2020', 30 | lib: { 31 | // Could also be a dictionary or array of multiple entry points. 32 | entry: [ 33 | './src/index.qwik.ts', 34 | './src/lib/server.tsx', 35 | './src/lib/slot.ts', 36 | './src/vite.ts', 37 | ], 38 | name: 'qwik-angular', 39 | // Change this to the formats you want to support. 40 | // Don't forget to update your package.json as well. 41 | formats: ['es', 'cjs'], 42 | fileName: (format, entryName) => 43 | `${entryName}.${format === 'es' ? 'mjs' : 'cjs'}`, 44 | }, 45 | rollupOptions: { 46 | // External packages that should not be bundled into your library. 47 | external: [ 48 | '@builder.io/qwik', 49 | '@builder.io/qwik/build', 50 | '@builder.io/qwik/server', 51 | '@builder.io/qwik/jsx-runtime', 52 | '@builder.io/qwik/jsx-dev-runtime', 53 | '@qwik-client-manifest', 54 | '@vite/client', 55 | '@vite/env', 56 | 'node-fetch', 57 | 'undici', 58 | '@angular/platform-browser', 59 | '@angular/platform-browser/animations', 60 | '@angular/common', 61 | '@angular/core', 62 | '@angular/platform-server', 63 | '@analogjs/vite-plugin-angular', 64 | 'rxjs', 65 | 'rxjs/operators', 66 | 'domino', 67 | 'sass', 68 | ], 69 | }, 70 | }, 71 | }); 72 | 73 | function vitePluginFixBundle(): Plugin { 74 | return { 75 | name: 'vite-plugin-fix-bundle', 76 | generateBundle(options, bundle) { 77 | // there're extra imports added to the index files, which breaks the library 78 | // removing them manually as it seems like there's no configuration option to do this 79 | ['index.qwik.cjs', 'index.qwik.mjs'] 80 | .map((f) => bundle?.[f]) 81 | .filter((c): c is OutputChunk => !!(c as OutputChunk)?.code) 82 | .forEach((chunk) => { 83 | chunk.code = chunk.code.replace( 84 | /^((import '.+')|(require\('.+'\)));\n/gm, 85 | '' 86 | ); 87 | }); 88 | }, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qwik-angular/source", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "local-registry": { 6 | "executor": "@nx/js:verdaccio", 7 | "options": { 8 | "port": 4873, 9 | "config": ".verdaccio/config.yml", 10 | "storage": "tmp/local-registry/storage" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tools/scripts/publish.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a minimal script to publish your package to "npm". 3 | * This is meant to be used as-is or customize as you see fit. 4 | * 5 | * This script is executed on "dist/path/to/library" as "cwd" by default. 6 | * 7 | * You might need to authenticate with NPM before running this script. 8 | */ 9 | 10 | import { execSync } from 'child_process'; 11 | import { readFileSync, writeFileSync } from 'fs'; 12 | 13 | import devkit from '@nx/devkit'; 14 | const { readCachedProjectGraph } = devkit; 15 | 16 | function invariant(condition, message) { 17 | if (!condition) { 18 | console.error(message); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | // Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag} 24 | // Default "tag" to "next" so we won't publish the "latest" tag by accident. 25 | const [, , name, version, tag = 'next'] = process.argv; 26 | 27 | // A simple SemVer validation to validate the version 28 | const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/; 29 | invariant( 30 | version && validVersion.test(version), 31 | `No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.` 32 | ); 33 | 34 | const graph = readCachedProjectGraph(); 35 | const project = graph.nodes[name]; 36 | 37 | invariant( 38 | project, 39 | `Could not find project "${name}" in the workspace. Is the project.json configured correctly?` 40 | ); 41 | 42 | const outputPath = project.data?.targets?.build?.options?.outputPath; 43 | invariant( 44 | outputPath, 45 | `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?` 46 | ); 47 | 48 | process.chdir(outputPath); 49 | 50 | // Updating the version in "package.json" before publishing 51 | try { 52 | const json = JSON.parse(readFileSync(`package.json`).toString()); 53 | json.version = version; 54 | writeFileSync(`package.json`, JSON.stringify(json, null, 2)); 55 | } catch (e) { 56 | console.error(`Error reading package.json file from library build output.`); 57 | } 58 | 59 | // Execute "npm publish" to publish 60 | execSync(`npm publish --access public --tag ${tag}`); 61 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "add-angular-to-qwik": ["packages/add-angular-to-qwik/src/index.ts"], 19 | "qwik-angular": ["packages/qwik-angular/src/index.ts"], 20 | "qwik-angular/vite": ["packages/qwik-angular/src/vite.ts"], 21 | "qwik-angular2": ["packages/qwik-angular2/src/index.ts"] 22 | } 23 | }, 24 | "exclude": ["node_modules", "tmp"] 25 | } 26 | --------------------------------------------------------------------------------