├── .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}>${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 |
--------------------------------------------------------------------------------